def get_sky_TE(optical_system, plotit=True): " Sky thermal background photon flux in the J, H and K bands " detector = optical_system.detector telescope = optical_system.telescope cryostat = optical_system.cryostat sky = optical_system.sky I_sky = { 'J' : 0.0, 'H' : 0.0, 'K' : 0.0 } # Atmospheric properties # eps_sky = get_sky_emissivity() for key in I_sky: wavelength_min = FILTER_BANDS_M[key][2] wavelength_max = FILTER_BANDS_M[key][3] I_sky[key] = etcutils.thermal_emission_intensity( T = sky.T, wavelength_min = wavelength_min, wavelength_max = wavelength_max, Omega = optical_system.omega_px_sr, A = telescope.A_collecting_m2, eps = sky.eps, eta = detector.qe * telescope.tau * cryostat.Tr_win ) if plotit: D = np.ones(1000)*detector.dark_current wavelengths = np.linspace(0.80, 2.5, 1000)*1e-6 # Plotting mu.newfigure(1,1) plt.plot(wavelengths*1e6, D, 'g--', label=r'Dark current') # Imager mode for key in I_sky: plt.errorbar(FILTER_BANDS_M[key][0]*1e6, I_sky[key], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='o') plt.text(FILTER_BANDS_M[key][0]*1e6, I_sky[key]*5, key) plt.yscale('log') plt.axis('tight') plt.ylim(ymax=100*I_sky['K'],ymin=1e-5) plt.legend(loc='lower right') plt.xlabel(r'$\lambda$ ($\mu$m)') plt.ylabel(r'Count ($e^{-}$ s$^{-1}$ pixel$^{-1}$)') plt.title(r'Estimated count from sky thermal emission') mu.show_plot() return I_sky
def convolve_psf(image, psf, padFactor=1, plotit=False): """ Convolve an input PSF with an input image. """ # Padding the source image. height, width = image.shape pad_ud = height // padFactor // 2 pad_lr = width // padFactor // 2 # If the image dimensions are odd, need to ad an extra row/column of zeros. image_padded = np.pad( image, ((pad_ud,pad_ud + height % 2), (pad_lr,pad_lr + width % 2)), mode='constant') conv_height = 2 * pad_ud + height + (height % 2) conv_width = 2 * pad_lr + width + (width % 2) # Convolving the kernel with the image. image_conv = np.ndarray((conv_height, conv_width)) image_conv_cropped = np.ndarray((height, width)) image_padded = np.pad(image, ((pad_ud,pad_ud + height % 2),(pad_lr,pad_lr + width % 2)), mode='constant') image_conv = fftwconvolve.fftconvolve(image_padded, psf, mode='same') image_conv_cropped = image_conv[pad_ud : height + pad_ud, pad_lr : width + pad_lr] if plotit: mu.newfigure(2,2) plt.suptitle('Seeing-limiting image') plt.subplot(2,2,1) plt.imshow(image) mu.colorbar() plt.title('Input image') plt.subplot(2,2,2) plt.imshow(psf) mu.colorbar() plt.title('Kernel') plt.subplot(2,2,3) plt.imshow(image_conv) mu.colorbar() plt.title('Convolved image (padded)') plt.subplot(2,2,4) plt.imshow(image_conv_cropped) mu.colorbar() plt.title('Convolved image (original size)') mu.show_plot() return image_conv_cropped
def plot_noise_sources(optical_system): """ Plot the empirical sky brightness, thermal sky emission, thermal telescope emission and dark current as a function of wavelength_m """ detector = optical_system.detector telescope = optical_system.telescope cryostat = optical_system.cryostat sky = optical_system.sky counts = { 'H' : 0, 'J' : 0, 'K' : 0 } counts['H'] = exposure_time_calc(band='H', t_exp=1, optical_system=optical_system) counts['J'] = exposure_time_calc(band='J', t_exp=1, optical_system=optical_system) counts['K'] = exposure_time_calc(band='K', t_exp=1, optical_system=optical_system) D = np.ones(1000)*detector.dark_current wavelengths = np.linspace(1.0, 2.5, 1000)*1e-6 # Plotting mu.newfigure(1.5,1.5) plt.plot(wavelengths*1e6, D, 'g--', label=r'Dark current') plotColors = { 'H' : 'orangered', 'J' : 'darkorange', 'K' : 'darkred' } for key in counts: if key == 'J': plt.errorbar(FILTER_BANDS_M[key][0]*1e6, counts[key]['gain-multiplied']['N_sky_emp'], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='o', ecolor=plotColors[key], mfc=plotColors[key], label='Empirical sky background') plt.errorbar(FILTER_BANDS_M[key][0]*1e6, counts[key]['gain-multiplied']['N_sky_thermal'], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='^', ecolor=plotColors[key], mfc=plotColors[key], label='Thermal sky background') plt.errorbar(FILTER_BANDS_M[key][0]*1e6, counts[key]['gain-multiplied']['N_tel'], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='*', ecolor=plotColors[key], mfc=plotColors[key], label='Thermal telescope background') plt.errorbar(FILTER_BANDS_M[key][0]*1e6, counts[key]['gain-multiplied']['N_tel'] + counts[key]['gain-multiplied']['N_sky_thermal'], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='x', ecolor=plotColors[key], mfc=plotColors[key], label='Thermal telescope + sky background') else: plt.errorbar(FILTER_BANDS_M[key][0]*1e6, counts[key]['gain-multiplied']['N_sky_emp'], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='o', ecolor=plotColors[key], mfc=plotColors[key]) plt.errorbar(FILTER_BANDS_M[key][0]*1e6, counts[key]['gain-multiplied']['N_sky_thermal'], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='^', ecolor=plotColors[key], mfc=plotColors[key]) plt.errorbar(FILTER_BANDS_M[key][0]*1e6, counts[key]['gain-multiplied']['N_tel'], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='*', ecolor=plotColors[key], mfc=plotColors[key]) plt.errorbar(FILTER_BANDS_M[key][0]*1e6, counts[key]['gain-multiplied']['N_tel'] + counts[key]['gain-multiplied']['N_sky_thermal'], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='x', ecolor=plotColors[key], mfc=plotColors[key]) plt.text(FILTER_BANDS_M[key][0]*1e6, counts[key]['gain-multiplied']['N_sky_emp']*5, key) plt.yscale('log') plt.axis('tight') plt.ylim(ymax=100*counts['K']['gain-multiplied']['N_tel'],ymin=1e-5) plt.legend(loc='lower right') plt.xlabel(r'$\lambda$ ($\mu$m)') plt.ylabel(r'Count ($e^{-}$ s$^{-1}$ pixel$^{-1}$)') plt.title(r'Expected background noise levels (gain-multiplied by %d)' % detector.gain) mu.show_plot()
def get_sky_TE(optical_system, plotit=True): " Sky thermal background photon flux in the J, H and K bands " detector = optical_system.detector telescope = optical_system.telescope cryostat = optical_system.cryostat sky = optical_system.sky I_sky = {'J': 0.0, 'H': 0.0, 'K': 0.0} # Atmospheric properties # eps_sky = get_sky_emissivity() for key in I_sky: wavelength_min = FILTER_BANDS_M[key][2] wavelength_max = FILTER_BANDS_M[key][3] I_sky[key] = etcutils.thermal_emission_intensity( T=sky.T, wavelength_min=wavelength_min, wavelength_max=wavelength_max, Omega=optical_system.omega_px_sr, A=telescope.A_collecting_m2, eps=sky.eps, eta=detector.qe * telescope.tau * cryostat.Tr_win) if plotit: D = np.ones(1000) * detector.dark_current wavelengths = np.linspace(0.80, 2.5, 1000) * 1e-6 # Plotting mu.newfigure(1, 1) plt.plot(wavelengths * 1e6, D, 'g--', label=r'Dark current') # Imager mode for key in I_sky: plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, I_sky[key], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='o') plt.text(FILTER_BANDS_M[key][0] * 1e6, I_sky[key] * 5, key) plt.yscale('log') plt.axis('tight') plt.ylim(ymax=100 * I_sky['K'], ymin=1e-5) plt.legend(loc='lower right') plt.xlabel(r'$\lambda$ ($\mu$m)') plt.ylabel(r'Count ($e^{-}$ s$^{-1}$ pixel$^{-1}$)') plt.title(r'Estimated count from sky thermal emission') mu.show_plot() return I_sky
def convolve_psf(image, psf, padFactor=1, plotit=False): """ Convolve an input PSF with an input image. """ # Padding the source image. height, width = image.shape pad_ud = height // padFactor // 2 pad_lr = width // padFactor // 2 # If the image dimensions are odd, need to ad an extra row/column of zeros. image_padded = np.pad(image, ((pad_ud, pad_ud + height % 2), (pad_lr, pad_lr + width % 2)), mode='constant') conv_height = 2 * pad_ud + height + (height % 2) conv_width = 2 * pad_lr + width + (width % 2) # Convolving the kernel with the image. image_conv = np.ndarray((conv_height, conv_width)) image_conv_cropped = np.ndarray((height, width)) image_padded = np.pad(image, ((pad_ud, pad_ud + height % 2), (pad_lr, pad_lr + width % 2)), mode='constant') image_conv = fftwconvolve.fftconvolve(image_padded, psf, mode='same') image_conv_cropped = image_conv[pad_ud:height + pad_ud, pad_lr:width + pad_lr] if plotit: mu.newfigure(2, 2) plt.suptitle('Seeing-limiting image') plt.subplot(2, 2, 1) plt.imshow(image) mu.colorbar() plt.title('Input image') plt.subplot(2, 2, 2) plt.imshow(psf) mu.colorbar() plt.title('Kernel') plt.subplot(2, 2, 3) plt.imshow(image_conv) mu.colorbar() plt.title('Convolved image (padded)') plt.subplot(2, 2, 4) plt.imshow(image_conv_cropped) mu.colorbar() plt.title('Convolved image (original size)') mu.show_plot() return image_conv_cropped
def field_star(psf, band, mag, optical_system, star_coords_as, final_sz, plate_scale_as_px, gain=1, magnitude_system='AB', plotit=False): """ Returns an image of a star in a field with a specified position offset (specified w.r.t. the centre of the image). The returned image IS NOT gain-multiplied by default. Be careful! """ # Scale up to the correct magnitude star = psf * etcutils.surface_brightness_to_count_rate( mu=mag, A_tel=optical_system.telescope.A_collecting_m2, tau=optical_system.telescope.tau, qe=optical_system.detector.qe, gain=gain, magnitude_system=magnitude_system, band=band) # Pad the sides appropriately. star_coords_px = [int(x / plate_scale_as_px) for x in star_coords_as] pad_ud, pad_lr = (int((x - y) // 2) for x, y in zip(final_sz, psf.shape)) star_padded = np.pad(array=star, pad_width=((pad_ud + star_coords_px[0], pad_ud - star_coords_px[0]), (pad_lr + star_coords_px[1], pad_lr - star_coords_px[1])), mode='constant') if plotit: mu.newfigure(1, 2) plt.suptitle("Field star image") mu.astroimshow(im=psf, plate_scale_as_px=plate_scale_as_px, title="PSF", subplot=121) mu.astroimshow(im=star_padded, plate_scale_as_px=plate_scale_as_px, title='Moved to coordinates ({:.2f}",{:.2f}")'.format( star_coords_as[0], star_coords_as[1]), subplot=122) mu.show_plot() return star_padded
def field_star(psf, band, mag, optical_system, star_coords_as, final_sz, plate_scale_as_px, gain = 1, magnitude_system = 'AB', plotit = False ): """ Returns an image of a star in a field with a specified position offset (specified w.r.t. the centre of the image). The returned image IS NOT gain-multiplied by default. Be careful! """ # Scale up to the correct magnitude star = psf * etcutils.surface_brightness_to_count_rate( mu = mag, A_tel = optical_system.telescope.A_collecting_m2, tau = optical_system.telescope.tau, qe = optical_system.detector.qe, gain = gain, magnitude_system = magnitude_system, band = band) # Pad the sides appropriately. star_coords_px = [int(x/plate_scale_as_px) for x in star_coords_as] pad_ud, pad_lr = ( int((x - y) // 2) for x, y in zip(final_sz, psf.shape) ) star_padded = np.pad( array = star, pad_width = ( (pad_ud + star_coords_px[0], pad_ud - star_coords_px[0]), (pad_lr + star_coords_px[1], pad_lr - star_coords_px[1]) ), mode='constant') if plotit: mu.newfigure(1,2) plt.suptitle("Field star image") mu.astroimshow( im = psf, plate_scale_as_px = plate_scale_as_px, title="PSF", subplot=121) mu.astroimshow( im = star_padded, plate_scale_as_px = plate_scale_as_px, title='Moved to coordinates ({:.2f}",{:.2f}")'.format( star_coords_as[0], star_coords_as[1]), subplot=122) mu.show_plot() return star_padded
def plot_alignment_err_histogram(errs_as, li_method=''): x_errs_as = errs_as[:, 0] y_errs_as = errs_as[:, 1] # Plot a pretty histogram showing the distribution of the alignment errors, and fit a Gaussian to them. range_as = 2 * max(max(np.abs(y_errs_as)), max(np.abs(x_errs_as))) nbins = int(errs_as.shape[0] / 100) mu.newfigure(1.5, 1) plt.suptitle( '{} Lucky Imaging shifting-and-stacking alignment errors'.format( li_method)) plt.subplot(211) if nbins > 5: plt.hist(x_errs_as, bins=nbins, range=(-range_as / 2, +range_as / 2), normed=True) else: plt.hist(x_errs_as, range=(-range_as / 2, +range_as / 2), normed=True) mean_x = np.mean(x_errs_as) sigma_x = np.sqrt(np.var(x_errs_as)) x = np.linspace(-range_as / 2, range_as / 2, 100) plt.plot(x, normpdf(x, mean_x, sigma_x), 'r', label=r'$\sigma_x$ = %.4f"' % (sigma_x)) plt.title(r'$x$ alignment error') plt.xlabel('arcsec') plt.legend() plt.subplot(212) if nbins > 5: plt.hist(y_errs_as, bins=nbins, range=(-range_as / 2, +range_as / 2), normed=True) else: plt.hist(y_errs_as, range=(-range_as / 2, +range_as / 2), normed=True) mean_y = np.mean(y_errs_as) sigma_y = np.sqrt(np.var(y_errs_as)) y = np.linspace(-range_as / 2, range_as / 2, 100) plt.plot(y, normpdf(y, mean_y, sigma_y), 'r', label=r'$\sigma_y$ = %.4f"' % (sigma_y)) plt.title(r'$y$ alignment error') plt.xlabel('arcsec') plt.legend() mu.show_plot()
def sersic_2D(n, R_e, mu_e, theta_rad = 0, # Angle between major axis and detector horizontal (radians) i_rad = 0, # Inclination angle (radians; face-on corresponds to i = 0) R_max = None, # Plotting limit. Default is 20 * R_e R_trunc = np.inf, # Disc truncation radius gridsize = 500, # Number of returned grid points zeropoint = 0, wavelength_m = None, plotit = False, R_units = 'kpc' ): " Returns 2D Sersic intensity and surface brightness plots. " if R_max == None: R_max = 20 * R_e # Making a 2D intensity plot of the galaxy given its inclination and orientation. dR = 2 * R_max / gridsize scaleFactor = 2 imsize = gridsize*scaleFactor r = np.linspace(-R_max*scaleFactor, +R_max*scaleFactor, imsize) X, Y = np.meshgrid(r, r) if theta_rad != 0: print('WARNING: rotations are still kinda dodgy. Proceed with caution!') R = imutils.rotateAndCrop(image_in_array = np.sqrt(X * X + Y * Y / (np.cos(i_rad) * np.cos(i_rad))), angle=theta_rad * 180 / np.pi, cropArg=(imsize-gridsize)//2) # Calculating the Sersic flux and surface brightness profiles R, mu_map, F_map = sersic(n=n, R_e=R_e, R=R, mu_e=mu_e, zeropoint=zeropoint, wavelength_m=wavelength_m) # Truncating the profiles mu_map[R>R_trunc] = np.inf for key in F_map: F_map[key][R>R_trunc] = 0 if plotit: mu.newfigure(2,1) plt.subplot(1,2,1) plt.imshow(F_map['F_nu_cgs'], norm=LogNorm(), extent = [-dR*gridsize/2,dR*gridsize/2,-dR*gridsize/2,dR*gridsize/2]) plt.xlabel(r'$R$ (%s)' % R_units) plt.ylabel(r'$R$ (%s)' % R_units) mu.colorbar() plt.title('2D intensity map') plt.subplot(1,2,2) plt.imshow(mu_map, extent = [-dR*gridsize/2,dR*gridsize/2,-dR*gridsize/2,dR*gridsize/2]) plt.xlabel(r'$R$ (%s)' % R_units) plt.ylabel(r'$R$ (%s)' % R_units) mu.colorbar() plt.title('2D surface brightness map') plt.suptitle('2D Sersic profiles') mu.show_plot() return R, dR, F_map, mu_map
def image_from_fits(fname, plotit=False, idx=0): " Return an array of the image(s) stored in the FITS file fname. " if not fname.lower().endswith('fits'): fname += '.fits' hdulist = astropy.io.fits.open(fname) images_raw = hdulist[idx].data hdulist.close() images_raw, N, height, width = get_image_size(images_raw) if plotit: for k in range(N): plt.imshow(images_raw[k]) plt.title('Raw image %d from FITS file' % (k + 1)) plt.pause(0.1) mu.show_plot() return np.squeeze(images_raw), hdulist
def plot_alignment_err_histogram(errs_as, li_method=''): x_errs_as = errs_as[:,0] y_errs_as = errs_as[:,1] # Plot a pretty histogram showing the distribution of the alignment errors, and fit a Gaussian to them. range_as = 2 * max(max(np.abs(y_errs_as)), max(np.abs(x_errs_as))) nbins = int(errs_as.shape[0] / 100) mu.newfigure(1.5,1) plt.suptitle('{} Lucky Imaging shifting-and-stacking alignment errors'.format(li_method)) plt.subplot(211) if nbins > 5: plt.hist(x_errs_as, bins=nbins, range=(-range_as/2,+range_as/2), normed=True) else: plt.hist(x_errs_as, range=(-range_as/2,+range_as/2), normed=True) mean_x = np.mean(x_errs_as) sigma_x = np.sqrt(np.var(x_errs_as)) x = np.linspace(-range_as/2, range_as/2, 100) plt.plot(x, normpdf(x,mean_x,sigma_x), 'r', label=r'$\sigma_x$ = %.4f"' % (sigma_x)) plt.title(r'$x$ alignment error') plt.xlabel('arcsec') plt.legend() plt.subplot(212) if nbins > 5: plt.hist(y_errs_as, bins=nbins, range=(-range_as/2,+range_as/2), normed=True) else: plt.hist(y_errs_as, range=(-range_as/2,+range_as/2), normed=True) mean_y = np.mean(y_errs_as) sigma_y = np.sqrt(np.var(y_errs_as)) y = np.linspace(-range_as/2, range_as/2, 100) plt.plot(y, normpdf(y,mean_y,sigma_y), 'r', label=r'$\sigma_y$ = %.4f"' % (sigma_y)) plt.title(r'$y$ alignment error') plt.xlabel('arcsec') plt.legend() mu.show_plot()
def get_telescope_TE(optical_system, plotit=True): detector = optical_system.detector telescope = optical_system.telescope cryostat = optical_system.cryostat sky = optical_system.sky I_tel = { 'J' : 0.0, 'H' : 0.0, 'K' : 0.0 } for key in I_tel: wavelength_min = FILTER_BANDS_M[key][2] wavelength_max = FILTER_BANDS_M[key][3] # Mirrors # Assumptions: # 1. The area we use for the etendue is the collecting (i.e. reflective) area of the telescope, not the total area. # 2. For now we are ignoring the baffle on M2. # 3. We are not assuming the worst case for the spider (i.e. it is still substantially reflective). But you should see how substantial of a difference it makes. Always lean towards the worst-case. I_mirrors = 0 for mirror in telescope.mirrors: I_mirrors += etcutils.thermal_emission_intensity( T = telescope.T, wavelength_min = wavelength_min, wavelength_max = wavelength_max, Omega = optical_system.omega_px_sr, A = telescope.A_collecting_m2, eps = mirror.eps_eff, eta = detector.qe * cryostat.Tr_win ) # Spider if telescope.has_spider: I_spider = etcutils.thermal_emission_intensity( T = telescope.T, wavelength_min = wavelength_min, wavelength_max = wavelength_max, Omega = optical_system.omega_px_sr, A = telescope.A_collecting_m2, eps = telescope.eps_spider_eff, eta = telescope.tau * detector.qe * cryostat.Tr_win)\ + etcutils.thermal_emission_intensity( T = sky.T, wavelength_min = wavelength_min, wavelength_max = wavelength_max, Omega = optical_system.omega_px_sr, A = telescope.A_collecting_m2, eps = lambda wavelength_m : (1 - telescope.eps_spider_eff) * sky.eps(wavelength_m), eta = telescope.tau * detector.qe * cryostat.Tr_win) # Cryostat window I_window = etcutils.thermal_emission_intensity( T = cryostat.T, wavelength_min = wavelength_min, wavelength_max = wavelength_max, Omega = optical_system.omega_px_sr, A = telescope.A_collecting_m2, eps = cryostat.eps_win, eta = detector.qe # No cryostat window or telescope throughput terms because the radiation from the walls doesn't pass through it ) I_tel[key] = I_mirrors + I_spider + I_window if plotit: D = np.ones(1000)*detector.dark_current wavelengths = np.linspace(0.80, 2.5, 1000)*1e-6 # Plotting mu.newfigure(1,1) plt.plot(wavelengths*1e6, D, 'g--', label=r'Dark current') for key in I_tel: plt.errorbar(FILTER_BANDS_M[key][0]*1e6, I_tel[key], 0, FILTER_BANDS_M[key][1]/2*1e6, fmt='o') plt.text(FILTER_BANDS_M[key][0]*1e6, I_tel[key]*5, key) plt.yscale('log') plt.axis('tight') plt.ylim(ymax=100*I_tel['K'],ymin=1e-5) plt.legend(loc='lower right') plt.xlabel(r'$\lambda$ ($\mu$m)') plt.ylabel(r'Count ($e^{-}$ s$^{-1}$ pixel$^{-1}$)') plt.title(r'Estimated count from telescope thermal emission') mu.show_plot() return I_tel
def get_seeing_limited_image(images, seeing_diameter_as, plate_scale_as=1, padFactor=1, plotit=False): """ Convolve a Gaussian PSF with an input image to simulate seeing with a FWHM of seeing_diameter_as. """ print("Seeing-limiting image(s)",end="") images, N, height, width = get_image_size(images) # Padding the source image. pad_ud = height // padFactor // 2 pad_lr = width // padFactor // 2 # If the image dimensions are odd, need to ad an extra row/column of zeros. image_padded = np.pad(images[0], ((pad_ud,pad_ud + height % 2),(pad_lr,pad_lr + width % 2)), mode='constant') # conv_height = image_padded.shape[0] # conv_width = image_padded.shape[1] conv_height = 2 * pad_ud + height + (height % 2) conv_width = 2 * pad_lr + width + (width % 2) # Generate a Gaussian kernel. kernel = np.zeros((conv_height, conv_width)) y_as = np.arange(-conv_width//2, +conv_width//2 + conv_width%2, 1) * plate_scale_as x_as = np.arange(-conv_height//2, +conv_height//2 + conv_height%2, 1) * plate_scale_as X, Y = np.meshgrid(x_as, y_as) sigma = seeing_diameter_as / (2 * np.sqrt(2 * np.log(2))) kernel = np.exp(-(np.power(X, 2) + np.power(Y, 2)) / (2 * np.power(sigma,2))) kernel /= sum(kernel.flatten()) kernel = np.pad(kernel, ((pad_ud, pad_ud + height % 2), (pad_lr, pad_lr + width % 2)), mode='constant') # Convolving the kernel with the image. image_seeing_limited = np.ndarray((N, conv_height, conv_width)) image_seeing_limited_cropped = np.ndarray((N, height, width)) for k in range(N): print('.',end="") image_padded = np.pad(images[k], ((pad_ud,pad_ud + height % 2),(pad_lr,pad_lr + width % 2)), mode='constant') image_seeing_limited[k] = fftwconvolve.fftconvolve(image_padded, kernel, mode='same') image_seeing_limited_cropped[k] = image_seeing_limited[k,pad_ud : height + pad_ud, pad_lr : width + pad_lr] if plotit: mu.newfigure(2,2) plt.suptitle('Seeing-limiting image') plt.subplot(2,2,1) plt.imshow(images[0]) mu.colorbar() plt.title('Input image') plt.subplot(2,2,2) plt.imshow(kernel, extent=axes_kernel) mu.colorbar() plt.title('Kernel') plt.subplot(2,2,3) plt.imshow(image_seeing_limited[0]) mu.colorbar() plt.title('Convolved image') plt.subplot(2,2,4) plt.imshow(image_seeing_limited_cropped[0]) mu.colorbar() plt.title('Cropped, convolved image') mu.show_plot() return np.squeeze(image_seeing_limited_cropped)
def get_diffraction_limited_image(image_truth, l_px_m, f_ratio, wavelength_m, f_ratio_in=None, wavelength_in_m=None, # f-ratio and imaging wavelength of the input image (if it has N_os > 1) N_OS_psf=4, detector_size_px=None, plotit=False): """ Convolve the PSF of a given telescope at a given wavelength with image_truth to simulate diffraction-limited imaging. It is assumed that the truth image has the appropriate plate scale of, but may be larger than, the detector. If the detector size is not given, then it is assumed that the input image and detector have the same dimensions. The flow should really be like this: 1. Generate the PSF with N_OS = 4, say. 2. Rescale the image to achieve the same plate scale. 3. Convolve. 4. Resample back down to the original plate scale. """ print("Diffraction-limiting truth image(s)...") image_truth, N, height, width = imutils.get_image_size(image_truth) # If the input image is already sampled by N_os > 1, then the PSF that we convolve with the image needs to add in quadrature with the PSF that has already been convolved with the image to get to the scaling we want. if f_ratio_in != None and wavelength_in_m != None: # Then we need to add the PSFs in quadrature. f_ratio_out = f_ratio wavelength_out_m = wavelength_m efl = 1 D_in = efl / f_ratio_in D_out = efl / f_ratio_out FWHM_in = wavelength_in_m / D_in FWHM_out = wavelength_out_m / D_out FWHM_prime = np.sqrt(FWHM_out**2 - FWHM_in**2) wavelength_prime_m = wavelength_in_m D_prime = wavelength_prime_m / FWHM_prime f_ratio_prime = efl / D_prime f_ratio = f_ratio_prime wavelength_m = wavelength_prime_m # Because we specify the PSF in terms of Nyquist sampling, we need to express N_OS in terms of the f ratio and wavelength of the input image. N_OS_input = wavelength_m * f_ratio / 2 / l_px_m / (np.deg2rad(206265 / 3600)) # Calculating the PSF psf = psf_airy_disk_kernel(wavelength_m=wavelength_m, N_OS=N_OS_psf, l_px_m=l_px_m) # TODO need to check that the PSF is not larger than image_truth_large # Convolving the PSF and the truth image to obtain the simulated diffraction-limited image # image_difflim = np.ndarray((N, height, width)) for k in range(N): # Resample the image up to the appropriate plate scale. image_truth_large = resizeImagesToDetector(image_truth[k], 1/N_OS_input, 1/N_OS_psf) # Convolve with the PSF. image_difflim_large = fftwconvolve.fftconvolve(image_truth_large, psf, mode='same') # Resize the image to its original plate scale. if k == 0: im = resizeImagesToDetector(image_difflim_large, 1/N_OS_psf, 1/N_OS_input) image_difflim = np.ndarray((N, im.shape[0], im.shape[1])) image_difflim[0] = im else: image_difflim[k] = resizeImagesToDetector(image_difflim_large, 1/N_OS_psf, 1/N_OS_input) if plotit: mu.newfigure(1,3) plt.subplot(1,3,1) plt.imshow(psf) mu.colorbar() plt.title('Diffraction-limited PSF of telescope') plt.subplot(1,3,2) plt.imshow(image_truth[0]) mu.colorbar() plt.title('Truth image') plt.subplot(1,3,3) plt.imshow(image_difflim[0]) mu.colorbar() plt.title('Diffraction-limited image') plt.suptitle('Diffraction-limiting image') mu.show_plot() return np.squeeze(image_difflim)
def airy_disc(wavelength_m, f_ratio, l_px_m, detector_size_px=None, trapz_oversampling=8, # Oversampling used in the trapezoidal rule approximation. coords=None, P_0=1, plotit=False): """ Returns the PSF of an optical system with a circular aperture given the f ratio, pixel and detector size at a given wavelength_m. If desired, an offset (measured from the top left corner of the detector) can be specified in vector coords = (x, y). The PSF is normalised such that the sum of every pixel in the PSF (extended to infinity) is equal to P_0 (unity by default), where P_0 is the total energy incident upon the telescope aperture. P_0 represents the *ideal* total energy in the airy disc (that is, the total energy incident upon the telescope aperture), whilst P_sum measures the actual total energy in the image (i.e. the pixel values). """ # Output image size detector_height_px, detector_width_px = detector_size_px[0:2] # Intensity map grid size # Oversampled image size oversampled_height_px = detector_height_px * trapz_oversampling oversampled_width_px = detector_width_px * trapz_oversampling # Coordinates of the centre of the Airy disc in the intensity map grid if coords == None: x_offset = oversampled_height_px/2 y_offset = oversampled_width_px/2 else: x_offset = coords[0] * trapz_oversampling y_offset = coords[1] * trapz_oversampling dx = oversampled_height_px/2 - x_offset dy = oversampled_width_px/2 - y_offset # Intensity map grid indices (in metres) x = np.arange(-oversampled_height_px//2, +oversampled_height_px//2 + oversampled_height_px%2 + 1, 1) + dx y = np.arange(-oversampled_width_px//2, +oversampled_width_px//2 + oversampled_width_px%2 + 1, 1) + dy x *= l_px_m / trapz_oversampling y *= l_px_m / trapz_oversampling Y, X = np.meshgrid(y, x) # Central intensity (W m^-2) I_0 = P_0 * np.pi / 4 / wavelength_m / wavelength_m / f_ratio / f_ratio # Calculating the Airy disc r = lambda x, y: np.pi / wavelength_m / f_ratio * np.sqrt(np.power(x,2) + np.power(y,2)) I_fun = lambda x, y : np.power((2 * scipy.special.jv(1, r(x,y)) / r(x,y)), 2) * I_0 I = I_fun(X,Y) # I = np.swapaxes(I,0,1) nan_idx = np.where(np.isnan(I)) if nan_idx[0].shape != (0,): I[nan_idx[0][0],nan_idx[1][0]] = I_0 # removing the NaN in the centre of the image if necessary """ Converting intensity values to count values in each pixel """ # Approximation using top-hat intensity profile in each pixel count_approx = I * l_px_m**2 / trapz_oversampling**2 count_approx = count_approx.astype(np.float64) # Approximation using trapezoidal rule count_cumtrapz = np.zeros((detector_height_px,detector_width_px)) cumsum = 0 for j in range(detector_width_px): for k in range(detector_height_px): px_grid = I[trapz_oversampling*k:trapz_oversampling*k+trapz_oversampling+1,trapz_oversampling*j:trapz_oversampling*j+trapz_oversampling+1] res1 = scipy.integrate.cumtrapz(px_grid, dx = l_px_m/trapz_oversampling, axis = 0, initial = 0) res2 = scipy.integrate.cumtrapz(res1[-1,:], dx = l_px_m/trapz_oversampling, initial = 0) count_cumtrapz[k,j] = res2[-1] # Total energy in image P_sum = sum(count_cumtrapz.flatten()) count_cumtrapz /= P_sum if plotit: mu.newfigure(1,2) plt.subplot(1,2,1) plt.imshow(I, norm=LogNorm()) mu.colorbar() plt.title('Intensity (oversampled by a factor of %d)' % trapz_oversampling) plt.subplot(1,2,2) plt.imshow(count_cumtrapz, norm=LogNorm()) mu.colorbar() plt.title('Count (via trapezoidal rule)') mu.show_plot() return count_cumtrapz, I, P_0, P_sum, I_0
def lucky_frame( im, # In electron counts/s. psf, # Normalised. scale_factor, t_exp, final_sz, tt = np.array([0, 0]), im_star = None, # In electron counts/s. noise_frame_gain_multiplied = 0, # Noise injected into the system that is multiplied up by the detector gain after conversion to counts via a Poisson distribution, e.g. sky background, emission from telescope, etc. Must have shape final_sz. It is assumed that this noise frame has already been multiplied up by the detector gain! noise_frame_post_gain = 0, # Noise injected into the system after gain multiplication, e.g. read noise. Must have shape final_sz. gain = 1, # Detector gain. detector_saturation=np.inf, # Detector saturation. plate_scale_as_px_conv = 1, # Only used for plotting. plate_scale_as_px = 1, # Only used for plotting. plotit=False): """ This function can be used to generate a short-exposure 'lucky' image that can be input to the Lucky Imaging algorithms. Input: one 'raw' countrate image of a galaxy; one PSF with which to convolve it (at the same plate scale) Output: a 'Lucky' exposure. Process: convolve with PSF --> resize to detector --> add tip and tilt (from a premade vector of tip/tilt values) --> convert to counts --> add noise --> subtract the master sky/dark current. """ # Convolve with PSF. im_raw = im im_convolved = obssim.convolve_psf(im_raw, psf) # Add a star to the field. We need to add the star at the convolution plate scale BEFORE we resize down because of the tip-tilt adding step! if is_numlike(im_star): if im_star.shape != im_convolved.shape: print("ERROR: the input image of the star MUST have the same size and plate scale as the image of the galaxy after convolution!") raise UserWarning im_convolved += im_star # Resize to detector (+ edge buffer). im_resized = imutils.fourier_resize( im = im_convolved, scale_factor = scale_factor, conserve_pixel_sum = True) # Add tip and tilt. To avoid edge effects, max(tt) should be less than or equal to the edge buffer. edge_buffer_px = (im.shape[0] - final_sz[0]) / 2 if edge_buffer_px > 0 and max(tt) > edge_buffer_px: print("WARNING: the edge buffer is less than the supplied tip and tilt by a margin of {:.2f} pixels! Shifted image will be clipped.".format(np.abs(edge_buffer_px - max(tt)))) im_tt = obssim.add_tt(image = im_resized, tt_idxs = tt)[0] # Crop back down to the detector size. if edge_buffer_px > 0: im_tt = imutils.centre_crop(im_tt, final_sz) # Convert to counts. Note that we apply the gain AFTER we convert to integer # counts. im_counts = etcutils.expected_count_to_count(im_tt, t_exp = t_exp) * gain # Add the pre-gain noise. Here, we assume that the noise frame has already # been multiplied by the gain before being passed into this function. im_noisy = im_counts + noise_frame_gain_multiplied # Add the post-gain noise (i.e. read noise) im_noisy += noise_frame_post_gain # Account for detector saturation im_noisy = np.clip(im_noisy, a_min=0, a_max=detector_saturation) if plotit: plate_scale_as_px = plate_scale_as_px_conv * scale_factor # Plotting mu.newfigure(1,3) plt.suptitle('Convolving input image with PSF and resizing to detector') mu.astroimshow(im=im_raw, title='Truth image (electrons/s)', plate_scale_as_px = plate_scale_as_px_conv, colorbar_on=True, subplot=131) mu.astroimshow(im=psf, title='Point spread function (normalised)', plate_scale_as_px = plate_scale_as_px_conv, colorbar_on=True, subplot=132) # mu.astroimshow(im=im_convolved, # title='Star added, convolved with PSF (electrons/s)', # plate_scale_as_px = plate_scale_as_px_conv, # colorbar_on=True, # subplot=143) mu.astroimshow(im=im_resized, title='Resized to detector plate scale (electrons/s)', plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=133) # Zooming in on the galaxy # mu.newfigure(1,4) # plt.suptitle('Convolving input image with PSF and resizing to detector') # mu.astroimshow(im=imutils.centre_crop(im=im_raw, units='arcsec', plate_scale_as_px=plate_scale_as_px_conv, sz_final=(6, 6)), title='Raw input image (electrons/s)', plate_scale_as_px = plate_scale_as_px_conv, colorbar_on=True, subplot=141) # mu.astroimshow(im=psf, title='Point spread function (normalised)', plate_scale_as_px = plate_scale_as_px_conv, colorbar_on=True, subplot=142) # mu.astroimshow(im=imutils.centre_crop(im=im_convolved, units='arcsec', plate_scale_as_px=plate_scale_as_px_conv, sz_final=(6, 6)), title='Star added, convolved with PSF (electrons/s)', plate_scale_as_px = plate_scale_as_px_conv, colorbar_on=True, subplot=143) # mu.astroimshow(im=imutils.centre_crop(im=im_resized, units='arcsec', plate_scale_as_px=plate_scale_as_px, sz_final=(6, 6)), title='Resized to detector plate scale (electrons/s)', plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=144) mu.newfigure(1,3) plt.suptitle('Adding tip and tilt, converting to integer counts and adding noise') mu.astroimshow(im=im_tt, title='Atmospheric tip and tilt added (electrons/s)', plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=131) mu.astroimshow(im=im_counts, title=r'Converted to integer counts and gain-multiplied by %d (electrons)' % gain, plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=132) mu.astroimshow(im=im_noisy, title='Noise added (electrons)', plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=133) # plt.subplot(1,4,4) plt.figure() x = np.linspace(-im_tt.shape[0]/2, +im_tt.shape[0]/2, im_tt.shape[0]) * plate_scale_as_px plt.plot(x, im_tt[:, im_tt.shape[1]/2], 'g', label='Electron count rate') plt.plot(x, im_counts[:, im_tt.shape[1]/2], 'b', label='Converted to integer counts ($t_{exp} = %.2f$ s)' % t_exp) plt.plot(x, im_noisy[:, im_tt.shape[1]/2], 'r', label='Noise added') plt.xlabel('arcsec') plt.ylabel('Pixel value (electrons)') plt.title('Linear profiles') plt.axis('tight') plt.legend(loc='lower left') mu.show_plot() return im_noisy
def plot_noise_sources(optical_system): """ Plot the empirical sky brightness, thermal sky emission, thermal telescope emission and dark current as a function of wavelength_m """ detector = optical_system.detector telescope = optical_system.telescope cryostat = optical_system.cryostat sky = optical_system.sky counts = {'H': 0, 'J': 0, 'K': 0} counts['H'] = exposure_time_calc(band='H', t_exp=1, optical_system=optical_system) counts['J'] = exposure_time_calc(band='J', t_exp=1, optical_system=optical_system) counts['K'] = exposure_time_calc(band='K', t_exp=1, optical_system=optical_system) D = np.ones(1000) * detector.dark_current wavelengths = np.linspace(1.0, 2.5, 1000) * 1e-6 # Plotting mu.newfigure(1.5, 1.5) plt.plot(wavelengths * 1e6, D, 'g--', label=r'Dark current') plotColors = {'H': 'orangered', 'J': 'darkorange', 'K': 'darkred'} for key in counts: if key == 'J': plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, counts[key]['gain-multiplied']['N_sky_emp'], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='o', ecolor=plotColors[key], mfc=plotColors[key], label='Empirical sky background') plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, counts[key]['gain-multiplied']['N_sky_thermal'], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='^', ecolor=plotColors[key], mfc=plotColors[key], label='Thermal sky background') plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, counts[key]['gain-multiplied']['N_tel'], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='*', ecolor=plotColors[key], mfc=plotColors[key], label='Thermal telescope background') plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, counts[key]['gain-multiplied']['N_tel'] + counts[key]['gain-multiplied']['N_sky_thermal'], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='x', ecolor=plotColors[key], mfc=plotColors[key], label='Thermal telescope + sky background') else: plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, counts[key]['gain-multiplied']['N_sky_emp'], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='o', ecolor=plotColors[key], mfc=plotColors[key]) plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, counts[key]['gain-multiplied']['N_sky_thermal'], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='^', ecolor=plotColors[key], mfc=plotColors[key]) plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, counts[key]['gain-multiplied']['N_tel'], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='*', ecolor=plotColors[key], mfc=plotColors[key]) plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, counts[key]['gain-multiplied']['N_tel'] + counts[key]['gain-multiplied']['N_sky_thermal'], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='x', ecolor=plotColors[key], mfc=plotColors[key]) plt.text(FILTER_BANDS_M[key][0] * 1e6, counts[key]['gain-multiplied']['N_sky_emp'] * 5, key) plt.yscale('log') plt.axis('tight') plt.ylim(ymax=100 * counts['K']['gain-multiplied']['N_tel'], ymin=1e-5) plt.legend(loc='lower right') plt.xlabel(r'$\lambda$ ($\mu$m)') plt.ylabel(r'Count ($e^{-}$ s$^{-1}$ pixel$^{-1}$)') plt.title(r'Expected background noise levels (gain-multiplied by %d)' % detector.gain) mu.show_plot()
def get_telescope_TE(optical_system, plotit=True): detector = optical_system.detector telescope = optical_system.telescope cryostat = optical_system.cryostat sky = optical_system.sky I_tel = {'J': 0.0, 'H': 0.0, 'K': 0.0} for key in I_tel: wavelength_min = FILTER_BANDS_M[key][2] wavelength_max = FILTER_BANDS_M[key][3] # Mirrors # Assumptions: # 1. The area we use for the etendue is the collecting (i.e. reflective) area of the telescope, not the total area. # 2. For now we are ignoring the baffle on M2. # 3. We are not assuming the worst case for the spider (i.e. it is still substantially reflective). But you should see how substantial of a difference it makes. Always lean towards the worst-case. I_mirrors = 0 for mirror in telescope.mirrors: I_mirrors += etcutils.thermal_emission_intensity( T=telescope.T, wavelength_min=wavelength_min, wavelength_max=wavelength_max, Omega=optical_system.omega_px_sr, A=telescope.A_collecting_m2, eps=mirror.eps_eff, eta=detector.qe * cryostat.Tr_win) # Spider if telescope.has_spider: I_spider = etcutils.thermal_emission_intensity( T = telescope.T, wavelength_min = wavelength_min, wavelength_max = wavelength_max, Omega = optical_system.omega_px_sr, A = telescope.A_collecting_m2, eps = telescope.eps_spider_eff, eta = telescope.tau * detector.qe * cryostat.Tr_win)\ + etcutils.thermal_emission_intensity( T = sky.T, wavelength_min = wavelength_min, wavelength_max = wavelength_max, Omega = optical_system.omega_px_sr, A = telescope.A_collecting_m2, eps = lambda wavelength_m : (1 - telescope.eps_spider_eff) * sky.eps(wavelength_m), eta = telescope.tau * detector.qe * cryostat.Tr_win) # Cryostat window I_window = etcutils.thermal_emission_intensity( T=cryostat.T, wavelength_min=wavelength_min, wavelength_max=wavelength_max, Omega=optical_system.omega_px_sr, A=telescope.A_collecting_m2, eps=cryostat.eps_win, eta=detector. qe # No cryostat window or telescope throughput terms because the radiation from the walls doesn't pass through it ) I_tel[key] = I_mirrors + I_spider + I_window if plotit: D = np.ones(1000) * detector.dark_current wavelengths = np.linspace(0.80, 2.5, 1000) * 1e-6 # Plotting mu.newfigure(1, 1) plt.plot(wavelengths * 1e6, D, 'g--', label=r'Dark current') for key in I_tel: plt.errorbar(FILTER_BANDS_M[key][0] * 1e6, I_tel[key], 0, FILTER_BANDS_M[key][1] / 2 * 1e6, fmt='o') plt.text(FILTER_BANDS_M[key][0] * 1e6, I_tel[key] * 5, key) plt.yscale('log') plt.axis('tight') plt.ylim(ymax=100 * I_tel['K'], ymin=1e-5) plt.legend(loc='lower right') plt.xlabel(r'$\lambda$ ($\mu$m)') plt.ylabel(r'Count ($e^{-}$ s$^{-1}$ pixel$^{-1}$)') plt.title(r'Estimated count from telescope thermal emission') mu.show_plot() return I_tel
def airy_disc( wavelength_m, f_ratio, l_px_m, detector_size_px=None, trapz_oversampling=8, # Oversampling used in the trapezoidal rule approximation. coords=None, P_0=1, plotit=False): """ Returns the PSF of an optical system with a circular aperture given the f ratio, pixel and detector size at a given wavelength_m. If desired, an offset (measured from the top left corner of the detector) can be specified in vector coords = (x, y). The PSF is normalised such that the sum of every pixel in the PSF (extended to infinity) is equal to P_0 (unity by default), where P_0 is the total energy incident upon the telescope aperture. P_0 represents the *ideal* total energy in the airy disc (that is, the total energy incident upon the telescope aperture), whilst P_sum measures the actual total energy in the image (i.e. the pixel values). """ # Output image size detector_height_px, detector_width_px = detector_size_px[0:2] # Intensity map grid size # Oversampled image size oversampled_height_px = detector_height_px * trapz_oversampling oversampled_width_px = detector_width_px * trapz_oversampling # Coordinates of the centre of the Airy disc in the intensity map grid if coords == None: x_offset = oversampled_height_px / 2 y_offset = oversampled_width_px / 2 else: x_offset = coords[0] * trapz_oversampling y_offset = coords[1] * trapz_oversampling dx = oversampled_height_px / 2 - x_offset dy = oversampled_width_px / 2 - y_offset # Intensity map grid indices (in metres) x = np.arange(-oversampled_height_px // 2, +oversampled_height_px // 2 + oversampled_height_px % 2 + 1, 1) + dx y = np.arange(-oversampled_width_px // 2, +oversampled_width_px // 2 + oversampled_width_px % 2 + 1, 1) + dy x *= l_px_m / trapz_oversampling y *= l_px_m / trapz_oversampling Y, X = np.meshgrid(y, x) # Central intensity (W m^-2) I_0 = P_0 * np.pi / 4 / wavelength_m / wavelength_m / f_ratio / f_ratio # Calculating the Airy disc r = lambda x, y: np.pi / wavelength_m / f_ratio * np.sqrt( np.power(x, 2) + np.power(y, 2)) I_fun = lambda x, y: np.power( (2 * scipy.special.jv(1, r(x, y)) / r(x, y)), 2) * I_0 I = I_fun(X, Y) # I = np.swapaxes(I,0,1) nan_idx = np.where(np.isnan(I)) if nan_idx[0].shape != (0, ): I[nan_idx[0][0], nan_idx[1] [0]] = I_0 # removing the NaN in the centre of the image if necessary """ Converting intensity values to count values in each pixel """ # Approximation using top-hat intensity profile in each pixel count_approx = I * l_px_m**2 / trapz_oversampling**2 count_approx = count_approx.astype(np.float64) # Approximation using trapezoidal rule count_cumtrapz = np.zeros((detector_height_px, detector_width_px)) cumsum = 0 for j in range(detector_width_px): for k in range(detector_height_px): px_grid = I[trapz_oversampling * k:trapz_oversampling * k + trapz_oversampling + 1, trapz_oversampling * j:trapz_oversampling * j + trapz_oversampling + 1] res1 = scipy.integrate.cumtrapz(px_grid, dx=l_px_m / trapz_oversampling, axis=0, initial=0) res2 = scipy.integrate.cumtrapz(res1[-1, :], dx=l_px_m / trapz_oversampling, initial=0) count_cumtrapz[k, j] = res2[-1] # Total energy in image P_sum = sum(count_cumtrapz.flatten()) count_cumtrapz /= P_sum if plotit: mu.newfigure(1, 2) plt.subplot(1, 2, 1) plt.imshow(I, norm=LogNorm()) mu.colorbar() plt.title('Intensity (oversampled by a factor of %d)' % trapz_oversampling) plt.subplot(1, 2, 2) plt.imshow(count_cumtrapz, norm=LogNorm()) mu.colorbar() plt.title('Count (via trapezoidal rule)') mu.show_plot() return count_cumtrapz, I, P_0, P_sum, I_0
def get_diffraction_limited_image( image_truth, l_px_m, f_ratio, wavelength_m, f_ratio_in=None, wavelength_in_m=None, # f-ratio and imaging wavelength of the input image (if it has N_os > 1) N_OS_psf=4, detector_size_px=None, plotit=False): """ Convolve the PSF of a given telescope at a given wavelength with image_truth to simulate diffraction-limited imaging. It is assumed that the truth image has the appropriate plate scale of, but may be larger than, the detector. If the detector size is not given, then it is assumed that the input image and detector have the same dimensions. The flow should really be like this: 1. Generate the PSF with N_OS = 4, say. 2. Rescale the image to achieve the same plate scale. 3. Convolve. 4. Resample back down to the original plate scale. """ print("Diffraction-limiting truth image(s)...") image_truth, N, height, width = imutils.get_image_size(image_truth) # If the input image is already sampled by N_os > 1, then the PSF that we convolve with the image needs to add in quadrature with the PSF that has already been convolved with the image to get to the scaling we want. if f_ratio_in != None and wavelength_in_m != None: # Then we need to add the PSFs in quadrature. f_ratio_out = f_ratio wavelength_out_m = wavelength_m efl = 1 D_in = efl / f_ratio_in D_out = efl / f_ratio_out FWHM_in = wavelength_in_m / D_in FWHM_out = wavelength_out_m / D_out FWHM_prime = np.sqrt(FWHM_out**2 - FWHM_in**2) wavelength_prime_m = wavelength_in_m D_prime = wavelength_prime_m / FWHM_prime f_ratio_prime = efl / D_prime f_ratio = f_ratio_prime wavelength_m = wavelength_prime_m # Because we specify the PSF in terms of Nyquist sampling, we need to express N_OS in terms of the f ratio and wavelength of the input image. N_OS_input = wavelength_m * f_ratio / 2 / l_px_m / (np.deg2rad( 206265 / 3600)) # Calculating the PSF psf = psf_airy_disk_kernel(wavelength_m=wavelength_m, N_OS=N_OS_psf, l_px_m=l_px_m) # TODO need to check that the PSF is not larger than image_truth_large # Convolving the PSF and the truth image to obtain the simulated diffraction-limited image # image_difflim = np.ndarray((N, height, width)) for k in range(N): # Resample the image up to the appropriate plate scale. image_truth_large = resizeImagesToDetector(image_truth[k], 1 / N_OS_input, 1 / N_OS_psf) # Convolve with the PSF. image_difflim_large = fftwconvolve.fftconvolve(image_truth_large, psf, mode='same') # Resize the image to its original plate scale. if k == 0: im = resizeImagesToDetector(image_difflim_large, 1 / N_OS_psf, 1 / N_OS_input) image_difflim = np.ndarray((N, im.shape[0], im.shape[1])) image_difflim[0] = im else: image_difflim[k] = resizeImagesToDetector(image_difflim_large, 1 / N_OS_psf, 1 / N_OS_input) if plotit: mu.newfigure(1, 3) plt.subplot(1, 3, 1) plt.imshow(psf) mu.colorbar() plt.title('Diffraction-limited PSF of telescope') plt.subplot(1, 3, 2) plt.imshow(image_truth[0]) mu.colorbar() plt.title('Truth image') plt.subplot(1, 3, 3) plt.imshow(image_difflim[0]) mu.colorbar() plt.title('Diffraction-limited image') plt.suptitle('Diffraction-limiting image') mu.show_plot() return np.squeeze(image_difflim)
def lucky_frame( im, # In electron counts/s. psf, # Normalised. scale_factor, t_exp, final_sz, tt=np.array([0, 0]), im_star=None, # In electron counts/s. noise_frame_gain_multiplied=0, # Noise injected into the system that is multiplied up by the detector gain after conversion to counts via a Poisson distribution, e.g. sky background, emission from telescope, etc. Must have shape final_sz. It is assumed that this noise frame has already been multiplied up by the detector gain! noise_frame_post_gain=0, # Noise injected into the system after gain multiplication, e.g. read noise. Must have shape final_sz. gain=1, # Detector gain. detector_saturation=np.inf, # Detector saturation. plate_scale_as_px_conv=1, # Only used for plotting. plate_scale_as_px=1, # Only used for plotting. plotit=False): """ This function can be used to generate a short-exposure 'lucky' image that can be input to the Lucky Imaging algorithms. Input: one 'raw' countrate image of a galaxy; one PSF with which to convolve it (at the same plate scale) Output: a 'Lucky' exposure. Process: convolve with PSF --> resize to detector --> add tip and tilt (from a premade vector of tip/tilt values) --> convert to counts --> add noise --> subtract the master sky/dark current. """ # Convolve with PSF. im_raw = im im_convolved = obssim.convolve_psf(im_raw, psf) # Add a star to the field. We need to add the star at the convolution plate scale BEFORE we resize down because of the tip-tilt adding step! if is_numlike(im_star): if im_star.shape != im_convolved.shape: print( "ERROR: the input image of the star MUST have the same size and plate scale as the image of the galaxy after convolution!" ) raise UserWarning im_convolved += im_star # Resize to detector (+ edge buffer). im_resized = imutils.fourier_resize(im=im_convolved, scale_factor=scale_factor, conserve_pixel_sum=True) # Add tip and tilt. To avoid edge effects, max(tt) should be less than or equal to the edge buffer. edge_buffer_px = (im.shape[0] - final_sz[0]) / 2 if edge_buffer_px > 0 and max(tt) > edge_buffer_px: print( "WARNING: the edge buffer is less than the supplied tip and tilt by a margin of {:.2f} pixels! Shifted image will be clipped." .format(np.abs(edge_buffer_px - max(tt)))) im_tt = obssim.add_tt(image=im_resized, tt_idxs=tt)[0] # Crop back down to the detector size. if edge_buffer_px > 0: im_tt = imutils.centre_crop(im_tt, final_sz) # Convert to counts. Note that we apply the gain AFTER we convert to integer # counts. im_counts = etcutils.expected_count_to_count(im_tt, t_exp=t_exp) * gain # Add the pre-gain noise. Here, we assume that the noise frame has already # been multiplied by the gain before being passed into this function. im_noisy = im_counts + noise_frame_gain_multiplied # Add the post-gain noise (i.e. read noise) im_noisy += noise_frame_post_gain # Account for detector saturation im_noisy = np.clip(im_noisy, a_min=0, a_max=detector_saturation) if plotit: plate_scale_as_px = plate_scale_as_px_conv * scale_factor # Plotting mu.newfigure(1, 3) plt.suptitle( 'Convolving input image with PSF and resizing to detector') mu.astroimshow(im=im_raw, title='Truth image (electrons/s)', plate_scale_as_px=plate_scale_as_px_conv, colorbar_on=True, subplot=131) mu.astroimshow(im=psf, title='Point spread function (normalised)', plate_scale_as_px=plate_scale_as_px_conv, colorbar_on=True, subplot=132) # mu.astroimshow(im=im_convolved, # title='Star added, convolved with PSF (electrons/s)', # plate_scale_as_px = plate_scale_as_px_conv, # colorbar_on=True, # subplot=143) mu.astroimshow(im=im_resized, title='Resized to detector plate scale (electrons/s)', plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=133) # Zooming in on the galaxy # mu.newfigure(1,4) # plt.suptitle('Convolving input image with PSF and resizing to detector') # mu.astroimshow(im=imutils.centre_crop(im=im_raw, units='arcsec', plate_scale_as_px=plate_scale_as_px_conv, sz_final=(6, 6)), title='Raw input image (electrons/s)', plate_scale_as_px = plate_scale_as_px_conv, colorbar_on=True, subplot=141) # mu.astroimshow(im=psf, title='Point spread function (normalised)', plate_scale_as_px = plate_scale_as_px_conv, colorbar_on=True, subplot=142) # mu.astroimshow(im=imutils.centre_crop(im=im_convolved, units='arcsec', plate_scale_as_px=plate_scale_as_px_conv, sz_final=(6, 6)), title='Star added, convolved with PSF (electrons/s)', plate_scale_as_px = plate_scale_as_px_conv, colorbar_on=True, subplot=143) # mu.astroimshow(im=imutils.centre_crop(im=im_resized, units='arcsec', plate_scale_as_px=plate_scale_as_px, sz_final=(6, 6)), title='Resized to detector plate scale (electrons/s)', plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=144) mu.newfigure(1, 3) plt.suptitle( 'Adding tip and tilt, converting to integer counts and adding noise' ) mu.astroimshow(im=im_tt, title='Atmospheric tip and tilt added (electrons/s)', plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=131) mu.astroimshow( im=im_counts, title= r'Converted to integer counts and gain-multiplied by %d (electrons)' % gain, plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=132) mu.astroimshow(im=im_noisy, title='Noise added (electrons)', plate_scale_as_px=plate_scale_as_px, colorbar_on=True, subplot=133) # plt.subplot(1,4,4) plt.figure() x = np.linspace(-im_tt.shape[0] / 2, +im_tt.shape[0] / 2, im_tt.shape[0]) * plate_scale_as_px plt.plot(x, im_tt[:, im_tt.shape[1] / 2], 'g', label='Electron count rate') plt.plot(x, im_counts[:, im_tt.shape[1] / 2], 'b', label='Converted to integer counts ($t_{exp} = %.2f$ s)' % t_exp) plt.plot(x, im_noisy[:, im_tt.shape[1] / 2], 'r', label='Noise added') plt.xlabel('arcsec') plt.ylabel('Pixel value (electrons)') plt.title('Linear profiles') plt.axis('tight') plt.legend(loc='lower left') mu.show_plot() return im_noisy
def get_seeing_limited_image(images, seeing_diameter_as, plate_scale_as=1, padFactor=1, plotit=False): """ Convolve a Gaussian PSF with an input image to simulate seeing with a FWHM of seeing_diameter_as. """ print("Seeing-limiting image(s)", end="") images, N, height, width = get_image_size(images) # Padding the source image. pad_ud = height // padFactor // 2 pad_lr = width // padFactor // 2 # If the image dimensions are odd, need to ad an extra row/column of zeros. image_padded = np.pad(images[0], ((pad_ud, pad_ud + height % 2), (pad_lr, pad_lr + width % 2)), mode='constant') # conv_height = image_padded.shape[0] # conv_width = image_padded.shape[1] conv_height = 2 * pad_ud + height + (height % 2) conv_width = 2 * pad_lr + width + (width % 2) # Generate a Gaussian kernel. kernel = np.zeros((conv_height, conv_width)) y_as = np.arange(-conv_width // 2, +conv_width // 2 + conv_width % 2, 1) * plate_scale_as x_as = np.arange(-conv_height // 2, +conv_height // 2 + conv_height % 2, 1) * plate_scale_as X, Y = np.meshgrid(x_as, y_as) sigma = seeing_diameter_as / (2 * np.sqrt(2 * np.log(2))) kernel = np.exp(-(np.power(X, 2) + np.power(Y, 2)) / (2 * np.power(sigma, 2))) kernel /= sum(kernel.flatten()) kernel = np.pad(kernel, ((pad_ud, pad_ud + height % 2), (pad_lr, pad_lr + width % 2)), mode='constant') # Convolving the kernel with the image. image_seeing_limited = np.ndarray((N, conv_height, conv_width)) image_seeing_limited_cropped = np.ndarray((N, height, width)) for k in range(N): print('.', end="") image_padded = np.pad(images[k], ((pad_ud, pad_ud + height % 2), (pad_lr, pad_lr + width % 2)), mode='constant') image_seeing_limited[k] = fftwconvolve.fftconvolve(image_padded, kernel, mode='same') image_seeing_limited_cropped[k] = image_seeing_limited[k, pad_ud:height + pad_ud, pad_lr:width + pad_lr] if plotit: mu.newfigure(2, 2) plt.suptitle('Seeing-limiting image') plt.subplot(2, 2, 1) plt.imshow(images[0]) mu.colorbar() plt.title('Input image') plt.subplot(2, 2, 2) plt.imshow(kernel, extent=axes_kernel) mu.colorbar() plt.title('Kernel') plt.subplot(2, 2, 3) plt.imshow(image_seeing_limited[0]) mu.colorbar() plt.title('Convolved image') plt.subplot(2, 2, 4) plt.imshow(image_seeing_limited_cropped[0]) mu.colorbar() plt.title('Cropped, convolved image') mu.show_plot() return np.squeeze(image_seeing_limited_cropped)
def simulate_sersic_galaxy(im_out_fname, # Output FITS file name height_px, # Height of output file width_px, # Width of output file mu_e, # Surface brightness magnitude at effective radius R_e_px, # Effective (half-light) radius (can be float) n, # Sersic index plate_scale_as_px, # Plate scale axis_ratio, # Axis ratio (a/b) zeropoint = -AB_MAGNITUDE_ZEROPOINT, # Careful of the minus sign! pos_px = None, # Position of galaxy in frame PA_deg = 0, # Rotation angle object_type = 'sersic/2', galfit_input_fname = None, plotit = False, overwrite_existing = False ): """ Return a simulated image of a galaxy (made using GALFIT) given the inputs. """ if galfit_input_fname == None: if not os.path.exists("galfit"): os.makedirs("galfit") galfit_input_fname = "galfit/galfit_input.txt" if not im_out_fname.endswith('.fits'): im_out_fname += '.fits' # Writing the parameters file. if not os.path.isfile(im_out_fname) or overwrite_existing: galfit_input_fname, im_out_fname = write_GALFIT_params_file( galfit_input_fname, im_out_fname, height_px, width_px, mu_e, R_e_px, n, plate_scale_as_px, axis_ratio, zeropoint, pos_px, PA_deg, object_type) # Calling GALFIT. call_GALFIT(galfit_input_fname) else: print("WARNING: I found a GALFIT .fits file '{}' with the same name as the input filename, so I am using that instead of calling GALFIT again!".format(im_out_fname)) # Editing the header to include the input parameters. hdulist = astropy.io.fits.open(im_out_fname, mode='update') if overwrite_existing: hdulist[0].header['R_E_PX'] = R_e_px hdulist[0].header['MU_E'] = mu_e hdulist[0].header['SER_IDX'] = n im_raw = hdulist[0].data hdulist.flush() hdulist.close() # Plotting. if plotit: mu.newfigure() plt.imshow(im_raw) plt.title("GALFIT-generated image") mu.colorbar() mu.show_plot() return im_raw