def propagate_one(wf, phase_screen=None, tiptilt=None, misalign=None, ngrid=1024, npupil=285, tag=None, onaxis=True, savefits=False, verbose=False, **conf): """ Propagate one single wavefront. An off-axis PSF can be obtained by switching onaxis to False, thereby decentering the focal plane mask (if any). """ # update conf conf.update(ngrid=ngrid, npupil=npupil) # keep a copy of the input wavefront wf1 = deepcopy(wf) # apply phase screen (scao residuals, ncpa, petal piston) if phase_screen is not None: phase_screen = np.nan_to_num(phase_screen) phase_screen = pad_img(resize_img(phase_screen, npupil), ngrid) proper.prop_add_phase(wf1, phase_screen) # apply tip-tilt (Zernike 2,3) if tiptilt is not None: # translate the tip/tilt from lambda/D into RMS phase errors # RMS = x/2 = ((lam/D)*(D/2))/2 = lam/4 tiptilt = np.array(tiptilt, ndmin=1)*conf['lam']/4 proper.prop_zernikes(wf1, [2,3], tiptilt) # update RAVC misalignment conf.update(ravc_misalign=misalign) # pupil-plane apodization wf1, apo_amp, apo_phase = apodizer(wf1, get_amp=True, verbose=verbose, **conf) # focal-plane mask, only in 'on-axis' configuration if onaxis == True: wf1 = fp_mask(wf1, verbose=verbose, **conf) # Lyot-stop or APP wf1, ls_amp, ls_phase = lyot_stop(wf1, get_amp=True, verbose=verbose, **conf) # detector psf = detector(wf1, verbose=verbose, **conf) # save psf as fits file if savefits == True: tag = '' if tag is None else '%s_'%tag name = '%s%s_PSF'%(tag, {True: 'onaxis', False: 'offaxis'}[onaxis]) save2fits(psf, name, **conf) return psf
def propagate_cube(wf, phase_screens, tiptilts, misaligns, cpu_count=1, send_to=None, tag=None, onaxis=True, savefits=False, verbose=False, **conf): # run simulation t0 = time.time() if cpu_count != 1 and platform in ['linux', 'linux2', 'darwin']: if cpu_count == None: cpu_count = mpro.cpu_count() - 1 if verbose is True: print('%s: e2e simulation starts, using %s cores'\ %(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), cpu_count)) p = mpro.Pool(cpu_count) func = partial(propagate_one, wf, onaxis=onaxis, verbose=False, **conf) psfs = np.array( p.starmap(func, zip(phase_screens, tiptilts, misaligns))) p.close() p.join() else: if verbose is True: print('%s: e2e simulation starts, using 1 core'\ %(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))) for i, (phase_screen, tiptilt, misalign) \ in enumerate(zip(phase_screens, tiptilts, misaligns)): psf = propagate_one(wf, phase_screen, tiptilt, misalign, \ onaxis=onaxis, verbose=False, **conf) psfs = psf if i == 0 else np.dstack((psfs.T, psf.T)).T if verbose is True: print('%s: finished, elapsed %.3f seconds\n'\ %(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), time.time() - t0)) # if only one wavefront, make dim = 2 if len(psfs) == 1: psfs = psfs[0] # save cube of PSFs to fits file, and notify by email if savefits == True: tag = '' if tag is None else '%s_' % tag name = '%s%s_PSF' % (tag, {True: 'onaxis', False: 'offaxis'}[onaxis]) filename = save2fits(psfs, name, **conf) notify('saved to %s' % filename, send_to) return psfs
def adi_one(dir_output='output_files', band='L', mode='RAVC', mag=5, mag_ref=0, flux_star=9e10, flux_bckg=9e4, add_bckg=True, pscale=5.47, cube_duration=3600, lat=-24.59, dec=-5, rim=19, app_strehl=0.64, app_single_psf=0.48, student_distrib=True, seed=123456, savepsf=False, savefits=False, verbose=False, **conf): """ This function calculates and draws the contrast curve (5-sigma sensitivity) for a specific set of off-axis PSF and on-axis PSF (or cube of PSFs), using the VIP package to perform ADI post-processing. Args: dir_output (str): path to output fits files and input PSFs (onaxis & offaxis) band (str): spectral band (e.g. 'L', 'M', 'N1', 'N2') mode (str): HCI mode: RAVC, CVC, APP, CLC mag (float): star magnitude at selected band mag_ref (float): reference magnitude for star and background fluxes flux_star (float): star flux at reference magnitude flux_bckg (float): background flux at reference magnitude add_bckg (bool) true means background flux and photon noise are added pscale (float): pixel scale in mas/pix (e.g. METIS LM=5.47, NQ=6.79) cube_duration (int): cube duration in seconds, default to 3600s (1h). lat (float): telescope latitude in deg (Armazones=-24.59 ,Paranal -24.63) dec (float): star declination in deg (e.g. 51 Eri -2.47) rim (int): psf image radius in pixels app_strehl (float): APP Strehl ratio app_single_psf (float): APP single PSF (4% leakage) student_distrib (bool): true if using a Student's t-distribution sensitivity, else Gaussian Return: sep (float ndarray): angular separation in arcsec sen (float ndarray): 5-sigma sensitivity (contrast) """ # PSF filenames loadname = os.path.join(dir_output, '%s_PSF_%s_%s.fits' % ('%s', band, mode)) # get normalized on-axis PSFs (star) psf_ON = fits.getdata(loadname % 'onaxis') if psf_ON.ndim != 3: # must be a cube (3D) psf_ON = np.array(psf_ON, ndmin=3) ncube = psf_ON.shape[0] # get normalized off-axis PSF (planet) psf_OFF = fits.getdata(loadname % 'offaxis') if psf_OFF.ndim == 3: # only one frame psf_OFF = psf_OFF[0, :, :] # calculate transmission trans = np.sum(psf_OFF) # apply correction for APP PSFs if mode == 'APP': psf_OFF *= app_single_psf * app_strehl psf_ON *= app_single_psf # detector integration time (DIT) DIT = cube_duration / ncube # rescale PSFs to star signal star_signal = DIT * flux_star * 10**(-0.4 * (mag - mag_ref)) psf_OFF *= star_signal psf_ON *= star_signal if verbose is True: print('Load PSFs for ADI') print(' mode=%s, band=%s, pscale=%s' % (mode, band, pscale)) print(' DIT=%3.3f, mag=%s, star_signal=%3.2E' % (DIT, mag, star_signal)) # add background and photon noise ~ N(0, sqrt(psf)) if add_bckg is True: bckg_noise = DIT * flux_bckg * trans psf_ON += bckg_noise np.random.seed(seed) phot_noise = np.random.normal(0, np.sqrt(psf_ON)) psf_ON += phot_noise if verbose is True: print(' add_bckg=%s, trans=%3.4f, bckg_noise=%3.2E'\ %(add_bckg, trans, bckg_noise)) """ VIP: aperture photometry of psf_OFF used to scale the contrast """ # get the center pixel (xoff, yoff) = psf_OFF.shape (cx, cy) = (int(xoff / 2), int(yoff / 2)) # fit a 2D Gaussian --> output: fwhm, x-y centroid fit = vip_hci.var.fit_2dgaussian(psf_OFF[cx-rim:cx+rim+1, \ cy-rim:cy+rim+1], True, (rim,rim), debug=False, full_output=True) # derive the FWHM fwhm = np.mean([fit['fwhm_x'], fit['fwhm_y']]) # recenter and crop shiftx, shifty = rim - fit['centroid_x'], rim - fit['centroid_y'] psf_OFF = vip_hci.preproc.frame_shift(psf_OFF, shiftx, shifty) psf_OFF_crop = psf_OFF[cx - rim:cx + rim + 1, cy - rim:cy + rim + 1] # FWHM aperture photometry of psf_OFF_crop starphot = vip_hci.metrics.aperture_flux(psf_OFF_crop, [rim], [rim], \ fwhm, verbose=False)[0] """ parallactic angles for ADI """ # duration -> hour angle conversion ha = cube_duration / 3600 / 24 * 360 # angles in rad hr = np.deg2rad(np.linspace(-ha / 2, ha / 2, ncube)) dr = np.deg2rad(dec) lr = np.deg2rad(lat) # parallactic angle in deg pa = -np.rad2deg(np.arctan2(-np.sin(hr), np.cos(dr)*np.tan(lr) \ - np.sin(dr)*np.cos(hr))) """ VIP: post-processing (ADI, ADI-PCA,...) """ # VIP post-processing algorithm algo = vip_hci.medsub.median_sub # contrast curve after post-processing (pscale in arcsec) cc_pp = vip_hci.metrics.contrast_curve(psf_ON, pa, psf_OFF_crop, \ fwhm, pscale/1e3, starphot, algo=algo, nbranch=1, sigma=5, \ debug=False, plot=False, verbose=verbose) # angular separations (in arcsec) sep = cc_pp.loc[:, 'distance_arcsec'].values # sensitivities (Student's or Gaussian distribution) distrib = 'sensitivity_student' if student_distrib == True else 'sensitivity_gaussian' sen = cc_pp.loc[:, distrib].values # save contrast curves as fits file if savefits == True: name = 'cc_adi_bckg%s_mag%s' % (int(add_bckg), mag) save2fits(np.array([sep, sen]), name, dir_output=dir_output, band=band, mode=mode) # psf after post-processing if savepsf is True: out, derot, psf_pp = algo(psf_ON, pa, full_output=True, verbose=False) name = 'psf_adi_bckg%s_mag%s' % (int(add_bckg), mag) save2fits(psf_pp, name, dir_output=dir_output, band=band, mode=mode) return sep, sen
def propagate_cube(wf, phase_screens, amp_screens, tiptilts, apo_misaligns, ls_misaligns, nframes=10, nstep=1, mode='RAVC', ngrid=1024, cpu_count=1, vc_chrom_leak=2e-3, add_cl_det=False, add_cl_vort=False, tag=None, onaxis=True, send_to=None, savefits=False, verbose=False, **conf): # update conf conf.update(mode=mode, ngrid=ngrid, cpu_count=cpu_count, vc_chrom_leak=vc_chrom_leak, add_cl_det=add_cl_det, add_cl_vort=add_cl_vort, tag=tag, onaxis=onaxis) # keep a copy of the input wavefront wf1 = deepcopy(wf) if verbose == True: print('Create %s-axis PSF cube' % {True: 'on', False: 'off'}[onaxis]) if add_cl_det is True: print(' adding chromatic leakage at detector plane: %s' % vc_chrom_leak) if add_cl_vort is True: print(' adding chromatic leakage at vortex plane: %s' % vc_chrom_leak) # preload amp screen if only one frame if len(amp_screens) == 1 and np.any(amp_screens) != None: proper.prop_multiply(wf1, pad_img(amp_screens, ngrid)) # then create a cube of None values amp_screens = [None] * int(nframes / nstep + 0.5) if verbose == True: print(' preloading amplitude screen') # preload apodizer when no drift if ('APP' in mode) or ('RAVC' in mode and np.all(apo_misaligns) == None): wf1 = apodizer(wf1, verbose=False, **conf) conf['apo_loaded'] = True if verbose == True: print(' preloading %s apodizer' % mode) else: conf['apo_loaded'] = False # preload Lyot stop when no drift if ('VC' in mode or 'LC' in mode) and np.all(ls_misaligns) == None: conf['ls_mask'] = lyot_stop(wf1, apply_ls=False, verbose=False, **conf) if verbose == True: print(' preloading Lyot stop') # run simulation posvars = [ phase_screens, amp_screens, tiptilts, apo_misaligns, ls_misaligns ] kwargs = dict(verbose=False, **conf) psfs = multiCPU(propagate_one, posargs=[wf1], posvars=posvars, kwargs=kwargs, case='e2e simulation', cpu_count=cpu_count) # if only one wavefront, make dim = 2 if len(psfs) == 1: psfs = psfs[0] # save cube of PSFs to fits file, and notify by email if savefits == True: tag = '' if tag is None else '%s_' % tag name = '%s%s_PSF' % (tag, {True: 'onaxis', False: 'offaxis'}[onaxis]) filename = save2fits(psfs, name, **conf) notify('saved to %s' % filename, send_to) return psfs
def propagate_one(wf, mode='RAVC', phase_screen=None, amp_screen=None, tiptilt=None, apo_misalign=None, ls_misalign=None, fp_offsets=None, ngrid=1024, npupil=285, vc_chrom_leak=2e-3, add_cl_det=False, tag=None, onaxis=True, savefits=False, verbose=False, **conf): """ Propagate one single wavefront. An off-axis PSF can be obtained by switching onaxis to False, thereby decentering the focal plane mask (if any). """ if verbose == True: print('Create single %s-axis PSF' % {True: 'on', False: 'off'}[onaxis]) # update conf conf.update(mode=mode, ngrid=ngrid, npupil=npupil, vc_chrom_leak=vc_chrom_leak, add_cl_det=add_cl_det, tag=tag, onaxis=onaxis) # keep a copy of the input wavefront wf1 = deepcopy(wf) # apply wavefront errors (SCAO residuals, NCPA, Talbot effect, ...) # and apodization (RAP, APP) wf1 = add_errors(wf1, phase_screen=phase_screen, amp_screen=amp_screen, tiptilt=tiptilt, apo_misalign=apo_misalign, verbose=verbose, **conf) # imaging a point source def point_source(wf1, verbose, conf): if onaxis == True: if add_cl_det is True and 'VC' in mode: # add chromatic leakage (vortex only) cl = deepcopy(wf1) cl._wfarr = np.flip(cl._wfarr) # 2 FFTs cl = lyot_stop(cl, ls_misalign=ls_misalign, verbose=verbose, **conf) chrom_leak = cl._wfarr * np.sqrt(vc_chrom_leak) else: chrom_leak = 0 wf1 = fp_mask(wf1, verbose=verbose, **conf) # focal-plane mask (onaxis only) wf1 = lyot_stop(wf1, ls_misalign=ls_misalign, verbose=verbose, **conf) wf1._wfarr += chrom_leak else: wf1._wfarr = np.flip(wf1._wfarr) # 2 FFTs wf1 = lyot_stop(wf1, ls_misalign=ls_misalign, verbose=verbose, **conf) return detector(wf1, verbose=verbose, **conf) # imaging a point source if fp_offsets is None: psf = point_source(wf1, verbose, conf) # imaging a finite size star else: psf = 0 for offset in fp_offsets: point = deepcopy(wf1) proper.prop_zernikes(point, [2, 3], np.array(offset, ndmin=1)) psf += point_source(point, False, conf) psf /= len(fp_offsets) # save psf as fits file if savefits == True: tag = '' if tag is None else '%s_' % tag name = '%s%s_PSF' % (tag, {True: 'onaxis', False: 'offaxis'}[onaxis]) save2fits(psf, name, **conf) return psf
def pupil(pup=None, f_pupil='', lam=3.8e-6, ngrid=1024, npupil=285, pupil_img_size=40, diam_ext=37, diam_int=11, spi_width=0.5, spi_angles=[0, 60, 120], npetals=6, seg_width=0, seg_gap=0, seg_rms=0, seg_ny=[ 10, 13, 16, 19, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 30, 31, 30, 31, 30, 31, 30, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 19, 16, 13, 10 ], seg_missing=[], select_petal=None, norm_I=True, savefits=False, verbose=False, **conf): ''' Create a wavefront object at the entrance pupil plane. The pupil is either loaded from a fits file, or created using pupil parameters. Can also select only one petal and mask the others. Args: dir_output (str): path to saved pupil file band (str): spectral band (e.g. 'L', 'M', 'N1', 'N2') mode (str): HCI mode: RAVC, CVC, APP, CLC f_pupil: str path to a pupil fits file lam: float wavelength in m ngrid: int number of pixels of the wavefront array npupil: int number of pixels of the pupil pupil_img_size: float pupil image (for PROPER) in m diam_ext: float outer circular aperture in m diam_int: float central obscuration in m spi_width: float spider width in m spi_angles: list of float spider angles in deg seg_width: float segment width in m seg_gap: float gap between segments in m seg_rms: float rms of the reflectivity of all segments seg_ny: list of int number of hexagonal segments per column (from left to right) seg_missing: list of tupples coordinates of missing segments npetals: int number of petals select_petal: int selected petal in range npetals, default to None ''' # initialize wavefront using PROPER beam_ratio = npupil / ngrid * (diam_ext / pupil_img_size) wf = proper.prop_begin(diam_ext, lam, ngrid, beam_ratio) # case 1: load pupil from data if pup is not None: if verbose is True: print("Load pupil data from 'pup'") pup = resize_img(pup, npupil) # case 2: load pupil from file elif os.path.isfile(f_pupil): if verbose is True: print("Load pupil from '%s'" % os.path.basename(f_pupil)) pup = resize_img(fits.getdata(f_pupil), npupil) # case 3: create a pupil else: if verbose is True: print("Create pupil: spi_width=%s m, seg_width=%s m, seg_gap=%s m, seg_rms=%s"\ %(spi_width, seg_width, seg_gap, seg_rms)) conf.update(npupil=npupil, pupil_img_size=pupil_img_size, diam_ext=diam_ext, diam_int=diam_int, spi_width=spi_width, spi_angles=spi_angles, seg_width=seg_width, seg_gap=seg_gap, seg_ny=seg_ny, seg_missing=seg_missing, seg_rms=seg_rms) pup = create_pupil(**conf) # select one petal (optional) if select_petal in range(npetals) and npetals > 1: if verbose is True: print(" select_petal=%s" % select_petal) petal = create_petal(select_petal, npetals, npupil) pup *= petal # normalize the entrance pupil intensity (total flux = 1) if norm_I is True: I_pup = pup**2 pup = np.sqrt(I_pup / np.sum(I_pup)) # save pupil as fits file if savefits == True: save2fits(pup, 'pupil', **conf) # pad with zeros and add to wavefront proper.prop_multiply(wf, pad_img(pup, ngrid)) return wf
def pupil(file_pupil='', lam=3.8e-6, ngrid=1024, npupil=285, pupil_img_size=40, diam_ext=37, diam_int=11, spi_width=0.5, spi_angles=[0, 60, 120], npetals=6, seg_width=0, seg_gap=0, seg_rms=0, seg_ny=[ 10, 13, 16, 19, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 30, 31, 30, 31, 30, 31, 30, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 19, 16, 13, 10 ], seg_missing=[], select_petal=None, savefits=False, verbose=False, **conf): ''' Create a wavefront object at the entrance pupil plane. The pupil is either loaded from a fits file, or created using pupil parameters. Can also select only one petal and mask the others. Args: dir_output (str): path to saved pupil file band (str): spectral band (e.g. 'L', 'M', 'N1', 'N2') mode (str): HCI mode: RAVC, CVC, APP, CLC file_pupil: str path to a pupil fits file lam: float wavelength in m ngrid: int number of pixels of the wavefront array npupil: int number of pixels of the pupil pupil_img_size: float pupil image (for PROPER) in m diam_ext: float outer circular aperture in m diam_int: float central obscuration in m spi_width: float spider width in m spi_angles: list of float spider angles in deg seg_width: float segment width in m seg_gap: float gap between segments in m seg_rms: float rms of the reflectivity of all segments seg_ny: list of int number of hexagonal segments per column (from left to right) seg_missing: list of tupples coordinates of missing segments npetals: int number of petals select_petal: int selected petal in range npetals, default to None ''' # initialize wavefront using PROPER beam_ratio = npupil / ngrid * (diam_ext / pupil_img_size) wf = proper.prop_begin(pupil_img_size, lam, ngrid, beam_ratio) # load pupil file if os.path.isfile(file_pupil): if verbose is True: print("Load pupil from '%s'" % os.path.basename(file_pupil)) #conf.update() pup = fits.getdata(file_pupil).astype(np.float32) # resize to npupil pup = resize_img(pup, npupil) # if no pupil file, create a pupil else: if verbose is True: print("Create pupil: spi_width=%s m, seg_width=%s m, seg_gap=%s m, seg_rms=%s"\ %(spi_width, seg_width, seg_gap, seg_rms)) conf.update(npupil=npupil, pupil_img_size=pupil_img_size, diam_ext=diam_ext, diam_int=diam_int, spi_width=spi_width, spi_angles=spi_angles, seg_width=seg_width, seg_gap=seg_gap, seg_ny=seg_ny, seg_missing=seg_missing, seg_rms=seg_rms) pup = create_pupil(**conf) # normalize the entrance pupil intensity (total flux = 1) I_pup = pup**2 pup = np.sqrt(I_pup / np.sum(I_pup)) # pad with zeros and add to wavefront proper.prop_multiply(wf, pad_img(pup, ngrid)) # select one petal (optional) if select_petal in range(npetals) and npetals > 1: if verbose is True: print(" select_petal=%s" % select_petal) # petal start and end angles pet_angle = 2 * np.pi / npetals pet_start = pet_angle / 2 + (select_petal - 1) * pet_angle pet_end = pet_start + pet_angle # petal angles must be 0-2pi ti %= (2 * np.pi) pet_start %= (2 * np.pi) pet_end %= (2 * np.pi) # check if petal crosses 0 angle if pet_end - pet_start < 0: pet_start = (pet_start + np.pi) % (2 * np.pi) - np.pi pet_end = (pet_end + np.pi) % (2 * np.pi) - np.pi ti = (ti + np.pi) % (2 * np.pi) - np.pi # create petal and add to grid petal = ((ti >= pet_start) * (ti <= pet_end)).astype(int) petal = resize_img(petal, ngrid) proper.prop_multiply(wf, petal) # save pupil as fits file if savefits == True: save2fits(pup, 'pupil', **conf) if verbose is True: print(' diam=%s m, resize to %s pix, zero-pad to %s pix\n'\ %(round(diam_ext, 2), npupil, ngrid)) return wf
def adi_one(dir_output='output_files', band='L', mode='RAVC', add_bckg=False, pscale=5.47, dit=0.3, mag=5, lat=-24.59, dec=-5, app_strehl=0.64, nscreens=None, ndet=None, tag=None, OAT=None, student_distrib=True, savepsf=False, savefits=False, rim=19, starphot=1e11, verbose=False, **conf): """ This function calculates and draws the contrast curve (5-sigma sensitivity) for a specific set of off-axis PSF and on-axis PSF (or cube of PSFs), using the VIP package to perform ADI post-processing. Args: dir_output (str): path to output fits files and input PSFs (onaxis & offaxis) band (str): spectral band (e.g. 'L', 'M', 'N1', 'N2') mode (str): HCI mode: RAVC, CVC, APP, CLC add_bckg (bool) true means background flux and photon noise are added pscale (float): pixel scale in mas/pix (e.g. METIS LM=5.47, NQ=6.79) dit (float): detector integration time in s mag (float): star magnitude at selected band lat (float): telescope latitude in deg (Armazones=-24.59 ,Paranal -24.63) dec (float): star declination in deg (e.g. 51 Eri -2.47) app_strehl (float): APP Strehl ratio nscreens (int): number of screens of PSF cube taken into account, default to None for full cube ndet (int): size of the screens at the detector, default to None for full size tag (str): tag added to the saved filename OAT (str): path to off-axis transmission file, default to None student_distrib (bool): true if using a Student's t-distribution sensitivity, else Gaussian savepsf (bool): true if ADI psf is saved in a fits file savefits (bool): true if ADI contrast curve is saved in a fits file rim (int): psf image radius in pixels starphot (float): normalization factor for aperture photometry with VIP Return: sep (float ndarray): angular separation in arcsec sen (float ndarray): 5-sigma sensitivity (contrast) """ # PSF filenames loadname = os.path.join(dir_output, '%s_PSF_%s_%s.fits' % ('%s', band, mode)) # get normalized on-axis PSFs (star) psf_ON = fits.getdata(loadname % 'onaxis') header_ON = fits.getheader(loadname % 'onaxis') assert psf_ON.ndim == 3, "on-axis PSF cube must be 3-dimensional" # cut/crop cube if nscreens != None: psf_ON = psf_ON[:nscreens] if ndet != None: psf_ON = crop_cube(psf_ON, ndet) # get normalized off-axis PSF (planet) psf_OFF = fits.getdata(loadname % 'offaxis') if psf_OFF.ndim == 3: psf_OFF = psf_OFF[0, :, :] # only first frame if verbose is True: print('Load PSFs for ADI') print(' mode=%s, band=%s' % (mode, band)) print(' ncube=%s, ndet=%s' % (psf_ON.shape[0], psf_ON.shape[1])) print(' pscale=%s mas, dit=%s s' % (pscale, dit)) # add background and photon noise: include star flux and HCI mode transmission if add_bckg is True: conf.update(mode=mode, dit=dit, mag=mag) psf_ON, psf_OFF = background(psf_ON, psf_OFF, header=header_ON, verbose=True, **conf) # apply APP Strehl if 'APP' in mode: psf_OFF *= app_strehl """ VIP: aperture photometry of psf_OFF used to scale the contrast """ # get the center pixel (xoff, yoff) = psf_OFF.shape (cx, cy) = (int(xoff / 2), int(yoff / 2)) # fit a 2D Gaussian --> output: fwhm, x-y centroid fit = vip_hci.var.fit_2dgaussian(psf_OFF[cx - rim:cx + rim + 1, cy - rim:cy + rim + 1], True, (rim, rim), debug=False, full_output=True) # derive the FWHM fwhm = np.mean([fit['fwhm_x'], fit['fwhm_y']]) # recenter and crop shiftx, shifty = rim - fit['centroid_x'], rim - fit['centroid_y'] psf_OFF = vip_hci.preproc.frame_shift(psf_OFF, shiftx, shifty) psf_OFF_crop = psf_OFF[cx - rim:cx + rim + 1, cy - rim:cy + rim + 1] # FWHM aperture photometry of psf_OFF_crop ap_flux = vip_hci.metrics.aperture_flux(psf_OFF_crop, [rim], [rim], fwhm, verbose=False)[0] # normalize to starphot (for VIP) if starphot is None: starphot = ap_flux else: psf_ON *= starphot / ap_flux psf_OFF_crop *= starphot / ap_flux """ parallactic angles for ADI """ # hour angle to deg conversion ha = 360 / 24 # angles in rad hr = np.deg2rad(np.linspace(-ha / 2, ha / 2, psf_ON.shape[0])) dr = np.deg2rad(dec) lr = np.deg2rad(lat) # parallactic angle in deg pa = -np.rad2deg( np.arctan2(-np.sin(hr), np.cos(dr) * np.tan(lr) - np.sin(dr) * np.cos(hr))) """ VIP: post-processing (ADI, ADI-PCA,...) """ # VIP post-processing algorithm algo = vip_hci.medsub.median_sub # get off-axis transmission if OAT != None: OAT = fits.getdata(OAT) OAT = (OAT[1], OAT[0]) # contrast curve after post-processing (pscale in arcsec) with warnings.catch_warnings(): warnings.simplefilter("ignore") # for AstropyDeprecationWarning cc_pp = vip_hci.metrics.contrast_curve(psf_ON, pa, psf_OFF_crop, fwhm, pscale / 1e3, starphot, algo=algo, nbranch=1, sigma=5, debug=False, plot=False, transmission=OAT, imlib='opencv', verbose=verbose) # angular separations (in arcsec) sep = cc_pp.loc[:, 'distance_arcsec'].values # sensitivities (Student's or Gaussian distribution) distrib = 'sensitivity_student' if student_distrib == True else 'sensitivity_gaussian' sen = cc_pp.loc[:, distrib].values # filename for fitsfiles if add_bckg is True: name = 'adi_bckg%s_mag%s' % (int(add_bckg), mag) else: name = 'adi_bckg%s' % int(add_bckg) # tag tag = '_%s' % tag.replace('/', '_') if tag != None else '' # save contrast curves as fits file if savefits == True: save2fits(np.array([sep, sen]), 'cc_%s%s%s' % (name, '_%s_%s', tag), dir_output=dir_output, band=band, mode=mode) # psf after post-processing if savepsf is True: _, _, psf_pp = algo(psf_ON, pa, full_output=True, verbose=False) save2fits(psf_pp, 'psf_%s%s%s' % (name, '_%s_%s', tag), dir_output=dir_output, band=band, mode=mode) return sep, sen
def propagate_one(wf, phase_screen=None, amp_screen=None, tiptilt=None, misalign=[0, 0, 0, 0, 0, 0], ngrid=1024, npupil=285, vc_chrom_leak=2e-3, add_cl_det=False, fp_offsets=None, tag=None, onaxis=True, savefits=False, verbose=False, **conf): """ Propagate one single wavefront. An off-axis PSF can be obtained by switching onaxis to False, thereby decentering the focal plane mask (if any). """ if verbose == True: print('Create single %s-axis PSF' % {True: 'on', False: 'off'}[onaxis]) # update conf conf.update(ngrid=ngrid, npupil=npupil, vc_chrom_leak=vc_chrom_leak, \ add_cl_det=add_cl_det, tag=tag, onaxis=onaxis) # keep a copy of the input wavefront wf1 = deepcopy(wf) # apply phase screen (scao residuals, ncpa, petal piston) wf1 = add_errors(wf1, phase_screen=phase_screen, amp_screen=amp_screen, \ tiptilt=tiptilt, misalign=misalign, verbose=verbose, **conf) # imaging a point source def point_source(wf1, verbose, conf): if onaxis == True: # focal-plane mask, only in 'on-axis' configuration if add_cl_det is True: cl = deepcopy(wf1) cl._wfarr = np.flip(cl._wfarr) # 2 FFTs cl = lyot_stop(cl, verbose=verbose, **conf) wf1 = fp_mask(wf1, verbose=verbose, **conf) wf1 = lyot_stop(wf1, verbose=verbose, **conf) if add_cl_det is True: wf1._wfarr += cl._wfarr * np.sqrt(vc_chrom_leak) else: wf1._wfarr = np.flip(wf1._wfarr) # 2 FFTs wf1 = lyot_stop(wf1, verbose=verbose, **conf) return detector(wf1, verbose=verbose, **conf) # imaging a point source if fp_offsets is None: psf = point_source(wf1, verbose, conf) # imaging a finite size star else: psf = 0 for offset in fp_offsets: point = deepcopy(wf1) proper.prop_zernikes(point, [2, 3], np.array(offset, ndmin=1)) psf += point_source(point, False, conf) psf /= len(fp_offsets) # save psf as fits file if savefits == True: tag = '' if tag is None else '%s_' % tag name = '%s%s_PSF' % (tag, {True: 'onaxis', False: 'offaxis'}[onaxis]) save2fits(psf, name, **conf) return psf
def propagate_cube(wf, phase_screens, amp_screens, tiptilts, misaligns, cpu_count=1, vc_chrom_leak=2e-3, add_cl_det=False, add_cl_vort=False, tag=None, onaxis=True, send_to=None, savefits=False, verbose=False, **conf): # update conf conf.update(cpu_count=cpu_count, vc_chrom_leak=vc_chrom_leak, \ add_cl_det=add_cl_det, add_cl_vort=add_cl_vort, tag=tag, onaxis=onaxis) # preload amp screen if only one frame if len(amp_screens) == 1 and np.any(amp_screens) != None: import proper from heeps.util.img_processing import pad_img, resize_img amp_screens = np.nan_to_num(amp_screens[0]) amp_screens = pad_img(resize_img(amp_screens, conf['npupil']), conf['ngrid']) proper.prop_multiply(wf, amp_screens) # then create a cube of None values amp_screens = [None] * int((conf['nframes'] / conf['nstep']) + 0.5) # preload apodizer when no drift if np.all(misaligns) == None or 'APP' in conf['mode']: wf = apodizer(wf, verbose=False, **conf) if verbose == True: print('Create %s-axis PSF cube' % {True: 'on', False: 'off'}[onaxis]) if add_cl_det is True: print(' adding chromatic leakage at detector plane: %s' % vc_chrom_leak) if add_cl_vort is True: print(' adding chromatic leakage at vortex plane: %s' % vc_chrom_leak) # run simulation posvars = [phase_screens, amp_screens, tiptilts, misaligns] kwargs = dict(verbose=False, **conf) psfs = multiCPU(propagate_one, posargs=[wf], posvars=posvars, kwargs=kwargs, \ case='e2e simulation', cpu_count=cpu_count) # if only one wavefront, make dim = 2 if len(psfs) == 1: psfs = psfs[0] # save cube of PSFs to fits file, and notify by email if savefits == True: tag = '' if tag is None else '%s_' % tag name = '%s%s_PSF' % (tag, {True: 'onaxis', False: 'offaxis'}[onaxis]) filename = save2fits(psfs, name, **conf) notify('saved to %s' % filename, send_to) return psfs
def propagate_cube(wf, phase_screens, amp_screens, tiptilts, misaligns, cpu_count=1, send_to=None, tag=None, onaxis=True, savefits=False, verbose=False, **conf): # preload amp screen if only one frame if len(amp_screens) == 1 and np.any(amp_screens) != None: import proper from heeps.util.img_processing import pad_img, resize_img amp_screens = np.nan_to_num(amp_screens[0]) amp_screens = pad_img(resize_img(amp_screens, conf['npupil']), conf['ngrid']) proper.prop_multiply(wf, amp_screens) # then create a cube of None values amp_screens = [None] * int((conf['nframes'] / conf['nstep']) + 0.5) # preload apodizer when no drift if np.all(misaligns) == None or 'APP' in conf['mode']: wf = apodizer(wf, verbose=False, **conf) if verbose == True: print('Create %s-axis PSF cube' % {True: 'on', False: 'off'}[onaxis]) # run simulation t0 = time.time() if cpu_count != 1 and platform in ['linux', 'linux2', 'darwin']: if cpu_count == None: cpu_count = mpro.cpu_count() - 1 if verbose is True: print(' %s: e2e simulation starts, using %s cores'\ %(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), cpu_count)) p = mpro.Pool(cpu_count) func = partial(propagate_one, wf, onaxis=onaxis, verbose=False, **conf) psfs = np.array( p.starmap(func, zip(phase_screens, amp_screens, tiptilts, misaligns))) p.close() p.join() else: if verbose is True: print(' %s: e2e simulation starts, using 1 core'\ %(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))) for i, (phase_screen, amp_screen, tiptilt, misalign) \ in enumerate(zip(phase_screens, amp_screens, tiptilts, misaligns)): psf = propagate_one(wf, phase_screen, amp_screen, tiptilt, misalign, \ onaxis=onaxis, verbose=True, **conf) psfs = psf if i == 0 else np.dstack((psfs.T, psf.T)).T if verbose is True: print(' %s: finished, elapsed %.3f seconds'\ %(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), time.time() - t0)) # if only one wavefront, make dim = 2 if len(psfs) == 1: psfs = psfs[0] # save cube of PSFs to fits file, and notify by email if savefits == True: tag = '' if tag is None else '%s_' % tag name = '%s%s_PSF' % (tag, {True: 'onaxis', False: 'offaxis'}[onaxis]) filename = save2fits(psfs, name, **conf) notify('saved to %s' % filename, send_to) return psfs