Beispiel #1
0
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
Beispiel #2
0
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
Beispiel #3
0
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
Beispiel #6
0
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
Beispiel #7
0
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
Beispiel #8
0
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
Beispiel #9
0
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
Beispiel #10
0
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
Beispiel #11
0
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