Example #1
0
def gen_cached_name(label, wfo):
    prefix = 'cached'
    # Most of the cacheable steps depend upon wfo for:
    ngrid = proper.prop_get_gridsize(wfo)
    beamradius = proper.prop_get_beamradius(wfo)
    sampling = proper.prop_get_sampling(wfo)
    return '{}_{}_{}_{}_{}.npy'.format(prefix, label, ngrid, sampling,
                                       beamradius)
Example #2
0
def quick_ao(wfo, iwf, f_lens, beam_ratio, iter, CPA_map):
    nact = tp.ao_act  # 49                       # number of DM actuators along one axis
    nact_across_pupil = nact - 2  # 47          # number of DM actuators across pupil
    dm_xc = (nact / 2) - 0.5
    dm_yc = (nact / 2) - 0.5
    d_beam = 2 * proper.prop_get_beamradius(wfo)  # beam diameter
    act_spacing = d_beam / nact_across_pupil  # actuator spacing
    # map_spacing = proper.prop_get_sampling(wfo)  # map sampling

    # dm_map = np.zeros((65, 65))
    # quicklook_im(CPA_map, logAmp=False, vmin=-3.14, vmax=3.14)
    dm_map = CPA_map[tp.grid_size / 2 -
                     (beam_ratio * tp.grid_size / 2):tp.grid_size / 2 +
                     (beam_ratio * tp.grid_size / 2) + 1, tp.grid_size / 2 -
                     (beam_ratio * tp.grid_size / 2):tp.grid_size / 2 +
                     (beam_ratio * tp.grid_size / 2) + 1]
    # quicklook_im(dm_map, logAmp=False)
    f = interpolate.interp2d(range(dm_map.shape[0]), range(dm_map.shape[0]),
                             dm_map)
    dm_map = f(np.linspace(0, dm_map.shape[0], nact),
               np.linspace(0, dm_map.shape[0], nact))
    # dm_map = proper.prop_magnify(CPA_map, map_spacing / act_spacing, nact)
    # quicklook_im(CPA_map, logAmp=False)
    # quicklook_im(dm_map, logAmp=False)
    # quicklook_wf(wfo)
    if tp.piston_error:
        # dprint('doing this')
        mean_dm_map = np.mean(np.abs(dm_map))
        # var = mean_dm_map/50.
        var = 0.001  # 1e-11
        dm_map = dm_map + np.random.normal(0, var,
                                           (dm_map.shape[0], dm_map.shape[1]))

    dm_map = -dm_map * proper.prop_get_wavelength(wfo) / (4 * np.pi
                                                          )  # <--- here
    # dmap = proper.prop_dm(wfo, dm_map, dm_xc, dm_yc, N_ACT_ACROSS_PUPIL=nact, FIT=True)  # <-- here
    dmap = proper.prop_dm(wfo, dm_map, dm_xc, dm_yc, act_spacing,
                          FIT=True)  # <-- here
    # quicklook_wf(wfo)
    # I = np.real(wfo.wfarr)
    # Q = np.imag(wfo.wfarr)
    # I = proper.prop_shift_center(I)
    # Q = proper.prop_shift_center(Q)
    #
    # anomaly = int(np.round(beam_ratio*tp.grid_size/2 + tp.grid_size/2))
    # # dprint((anomaly))
    # Q[anomaly] = 0 # 89
    # Q[:, anomaly] = 0
    # I[anomaly] = 0
    # I[:, anomaly] = 0
    # I = proper.prop_shift_center(I)
    # Q = proper.prop_shift_center(Q)
    # wfo.wfarr = I + 1j * Q

    return
Example #3
0
def deformable_mirror(wf, WFS_map, iter, plane_name=None):
    """
    combine different DM actuator commands into single map to send to prop_dm

    prop_dm needs an input map of n_actuators x n_actuators in units of actuator command height. quick_ao will handle
    the conversion to actuator command height, and the CDI probe must be scaled in cdip.probe_amp in params in
    units of m. Each subroutine is also responsible for creating a map of n_actuators x n_actuators spacing. prop_dm
    handles the resampling of this map onto the wavefront, including the influence function. Its some wizardry that
    happens in c, and presumably it is taken care of so you don't have to worry about it.

    In the call to proper.prop_dm, we apply the flag tp.fit_dm, which switches between two 'modes' of proper's DM
    surface fitting. If FALSE, the DM is driven to the heights specified by dm_map, and the influence function will
    act on these heights to define the final surface shape applied to the DM, which may differ substantially from
    the initial heights specified by dm_map. If TRUE, proper will iterate applying the influence function to the
    input heights, and adjust the heights until the difference between the influenced-map and input map meets some
    proper-defined convergence criterea. Setting tp.fit_dm=TRUE will obviously slow down the code, but will (likely)
    more accurately represent a well-calibrated DM response function.

    much of this code copied over from example from Proper manual on pg 94

    :param wf: single wavefront
    :param WFS_map: wavefront sensor map, should be in units of phase delay
    :param iter: the current index of iteration (which timestep this is)
    :param plane_name: name of plane (should be 'woofer' or 'tweeter' for best functionality
    :return: nothing is returned, but the probe map has been applied to the DM via proper.prop_dm
    """
    # AO Actuator Count from DM Type
    if plane_name == 'tweeter' and hasattr('tp', 'act_tweeter'):
        nact = tp.act_tweeter
    elif plane_name == 'woofer' and hasattr('tp', 'act_woofer'):
        nact = tp.act_woofer
    else:
        nact = tp.ao_act

    # DM Coordinates
    nact_across_pupil = nact - 2  # number of full DM actuators across pupil (oversizing DM extent)
    dm_xc = (
        nact / 2
    ) - 0.5  # The location of the optical axis (center of the wavefront) on the DM in
    dm_yc = (
        nact / 2
    ) - 0.5  # actuator units. First actuator is centered on (0.0, 0.0). The 0.5 is a
    #  parameter introduced/tuned by Rupert to remove weird errors (address this).
    # KD verified this needs to be here or else suffer weird errors 9/19
    # TODO address/remove the 0.5 in DM x,y coordinates

    ############################
    # Creating DM Surface Map
    ############################
    d_beam = 2 * proper.prop_get_beamradius(wf)  # beam diameter
    act_spacing = d_beam / nact_across_pupil  # actuator spacing [m]
    # map_spacing = proper.prop_get_sampling(wfo.wf_collection[iw,0])

    #######
    # AO
    #######
    dm_map = quick_ao(wf, nact, WFS_map[wf.iw])

    #######
    # CDI
    ######
    if cdip.use_cdi:
        # dprint(f"Applying CDI probe, lambda = {wfo.wsamples[iw]*1e9:.2f} nm")
        probe = cdi.CDIprobe(iter)
        # Add Probe to DM map
        dm_map = dm_map + probe

    #########################
    # Applying Piston Error
    #########################
    if tp.piston_error:
        mean_dm_map = np.mean(np.abs(dm_map))
        var = 1e-4  # 1e-11
        dm_map = dm_map + np.random.normal(0, var,
                                           (dm_map.shape[0], dm_map.shape[1]))

    #########################
    # proper.prop_dm
    #########################
    proper.prop_dm(wf, dm_map, dm_xc, dm_yc, act_spacing, FIT=tp.fit_dm)  #
    # proper.prop_dm(wfo, dm_map, dm_xc, dm_yc, N_ACT_ACROSS_PUPIL=nact, FIT=True)  #

    # check_sampling(0, wfo, "E-Field after DM", getframeinfo(stack()[0][0]), units='um')  # check sampling from optics.py

    return
Example #4
0
def quick_ao(wf, nact, WFS_map):
    """
    calculate the offset map to send to the DM from the WFS map

    The main idea is to apply the DM only to the region of the wavefront that contains the beam. The phase map from
    the wfs saved the whole wavefront, so that must be cropped. During the wavefront initialization in
    wavefront.initialize_proper, the beam ratio set in sp.beam_ratio is scaled per wavelength (to achieve constant
    sampling sto create white light images), so the cropped value must also be scaled by wavelength. Note, beam ratio
    is scaled differently depending on if sp.focused_sys is True or not. See params-->sp.focused_sys and Proper
    manual pg 36 for more info.

    Then, we interpolate the cropped beam onto a grid of (n_actuators,n_actuators), such that the DM can apply a
    actuator height to each represented actuator, not a over or sub-sampled form. If the number of actuators is low
    compared to the number of samples on the beam, you should anti-alias the WFS map via a lowpass filter before
    interpolating. There is a discrepancy between the sampling of the wavefront at this location (the size you cropped)
    vs the size of the DM. proper.prop_dm handles this, so just plug in the n_actuator sized DM map with specified
    parameters, and assume that prop_dm handles the resampling correctly via the spacing or n_act_across_pupil flag.
    FYI the resampling is done via a c library you installed/compiled when installing proper.

    The WFS map is a map of real values in units of phase delay in radians. However, the AO map that gets passed to
    proper.prop_dm wants input in nm height of each actuator. Therefore, you need to convert the phase delay to
    a DM height. For the ideal AO, you would do this individually for each wavelength. However, for a 'real' AO system
    you do this for the median wavelength. You also need to account for a factor of 2, since the DM is modeled as
    a mirror so it travels the length of the phase delay twice.

    much of this code copied over from example from Proper manual on pg 94

    :param wfo: wavefront object created by optics.Wavefronts() [n_wavelengths, n_objects] of tp.gridsize x tp.gridsize
    :param WFS_map: returned from quick_wfs (as of Aug 2019, its an idealized image)
    :return: ao_map: map of DM actuator command heights in units of m
    """

    nact_across_pupil = nact - 2  # number of full DM actuators across pupil (oversizing DM extent)
    # Note: oversample by 2 actuators hardcoded here, check if this is appropriate

    ############################
    # Creating AO Surface Map
    ############################
    d_beam = 2 * proper.prop_get_beamradius(wf)  # beam diameter
    act_spacing = d_beam / nact_across_pupil  # actuator spacing [m]
    # map_spacing = proper.prop_get_sampling(wfo.wf_collection[iw,0])

    ###################################
    # Cropping the Beam from WFS map
    ###################################
    # cropping here by beam_ratio rather than d_beam is valid since the beam size was initialized
    #  using the scaled beam_ratios when the wfo was created
    # dprint(f"{WFS_map[0,0,0]}")
    ao_map = WFS_map[
        sp.grid_size // 2 -
        np.int_(wf.beam_ratio * sp.grid_size // 2):sp.grid_size // 2 +
        np.int_(wf.beam_ratio * sp.grid_size // 2) + 1, sp.grid_size // 2 -
        np.int_(wf.beam_ratio * sp.grid_size // 2):sp.grid_size // 2 +
        np.int_(wf.beam_ratio * sp.grid_size // 2) + 1]

    ########################################################
    # Interpolating the WFS map onto the actuator spacing
    # (tp.nact,tp.nact)
    ########################################################
    # Lowpass Filter- prevents aliasing; uses Gaussian filter
    nyquist_dm = tp.ao_act / 2 * act_spacing  # [m]
    sigma = [nyquist_dm / 2.355,
             nyquist_dm / 2.355]  # assume we want sigma to be twice the HWHM
    ao_map = ndimage.gaussian_filter(ao_map, sigma=sigma, mode='nearest')

    f = interpolate.interp2d(range(ao_map.shape[0]),
                             range(ao_map.shape[0]),
                             ao_map,
                             kind='cubic')
    ao_map = f(np.linspace(0, ao_map.shape[0], nact),
               np.linspace(0, ao_map.shape[0], nact))
    # dm_map = proper.prop_magnify(dm_map, map_spacing / act_spacing, nact)

    ################################################
    # Converting phase delay to DM actuator height
    ################################################
    # Apply the inverse of the WFS image to the DM, so use -dm_map (dm_map is in phase units, divide by k=2pi/lambda)
    surf_height = proper.prop_get_wavelength(wf) / (4 * np.pi)  # [m/rad]
    ao_map = -ao_map * surf_height  # Converts DM map to units of [m] of actuator heights

    return ao_map
Example #5
0
def wfirst_phaseb(lambda_m, output_dim0, PASSVALUE={'dummy': 0}):

    # "output_dim" is used to specify the output dimension in pixels at the final image plane.
    # Computational grid sizes are hardcoded for each coronagraph.
    # Based on Zemax prescription "WFIRST_CGI_DI_LOWFS_Sep24_2018.zmx" by Hong Tang.

    data_dir = wfirst_phaseb_proper.data_dir
    if 'PASSVALUE' in locals():
        if 'data_dir' in PASSVALUE: data_dir = PASSVALUE['data_dir']

    map_dir = data_dir + wfirst_phaseb_proper.map_dir
    polfile = data_dir + wfirst_phaseb_proper.polfile

    cor_type = 'hlc'  # coronagraph type ('hlc', 'spc', 'none')
    source_x_offset_mas = 0  # source offset in mas (tilt applied at primary)
    source_y_offset_mas = 0
    source_x_offset = 0  # source offset in lambda0_m/D radians (tilt applied at primary)
    source_y_offset = 0
    polaxis = 0  # polarization axis aberrations:
    #    -2 = -45d in, Y out
    #    -1 = -45d in, X out
    #     1 = +45d in, X out
    #     2 = +45d in, Y out
    #     5 = mean of modes -1 & +1 (X channel polarizer)
    #     6 = mean of modes -2 & +2 (Y channel polarizer)
    #    10 = mean of all modes (no polarization filtering)
    use_errors = 1  # use optical surface phase errors? 1 or 0
    zindex = np.array([0, 0])  # array of Zernike polynomial indices
    zval_m = np.array([0, 0])  # array of Zernike coefficients (meters RMS WFE)
    use_aperture = 0  # use apertures on all optics? 1 or 0
    cgi_x_shift_pupdiam = 0  # X,Y shear of wavefront at FSM (bulk displacement of CGI); normalized relative to pupil diameter
    cgi_y_shift_pupdiam = 0
    cgi_x_shift_m = 0  # X,Y shear of wavefront at FSM (bulk displacement of CGI) in meters
    cgi_y_shift_m = 0
    fsm_x_offset_mas = 0  # offset in focal plane caused by tilt of FSM in mas
    fsm_y_offset_mas = 0
    fsm_x_offset = 0  # offset in focal plane caused by tilt of FSM in lambda0/D
    fsm_y_offset = 0
    end_at_fsm = 0  # end propagation after propagating to FSM (no FSM errors)
    focm_z_shift_m = 0  # offset (meters) of focus correction mirror (+ increases path length)
    use_hlc_dm_patterns = 0  # use Dwight's HLC default DM wavefront patterns? 1 or 0
    use_dm1 = 0  # use DM1? 1 or 0
    use_dm2 = 0  # use DM2? 1 or 0
    dm_sampling_m = 0.9906e-3  # actuator spacing in meters
    dm1_xc_act = 23.5  # for 48x48 DM, wavefront centered at actuator intersections: (0,0) = 1st actuator center
    dm1_yc_act = 23.5
    dm1_xtilt_deg = 0  # tilt around X axis (deg)
    dm1_ytilt_deg = 5.7  # effective DM tilt in deg including 9.65 deg actual tilt and pupil ellipticity
    dm1_ztilt_deg = 0  # rotation of DM about optical axis (deg)
    dm2_xc_act = 23.5  # for 48x48 DM, wavefront centered at actuator intersections: (0,0) = 1st actuator center
    dm2_yc_act = 23.5
    dm2_xtilt_deg = 0  # tilt around X axis (deg)
    dm2_ytilt_deg = 5.7  # effective DM tilt in deg including 9.65 deg actual tilt and pupil ellipticity
    dm2_ztilt_deg = 0  # rotation of DM about optical axis (deg)
    use_pupil_mask = 1  # SPC only: use SPC pupil mask (0 or 1)
    mask_x_shift_pupdiam = 0  # X,Y shear of shaped pupil mask; normalized relative to pupil diameter
    mask_y_shift_pupdiam = 0
    mask_x_shift_m = 0  # X,Y shear of shaped pupil mask in meters
    mask_y_shift_m = 0
    use_fpm = 1  # use occulter? 1 or 0
    fpm_x_offset = 0  # FPM x,y offset in lambda0/D
    fpm_y_offset = 0
    fpm_x_offset_m = 0  # FPM x,y offset in meters
    fpm_y_offset_m = 0
    fpm_z_shift_m = 0  # occulter offset in meters along optical axis (+ = away from prior optics)
    pinhole_diam_m = 0  # FPM pinhole diameter in meters
    end_at_fpm_exit_pupil = 0  # return field at FPM exit pupil?
    output_field_rootname = ''  # rootname of FPM exit pupil field file (must set end_at_fpm_exit_pupil=1)
    use_lyot_stop = 1  # use Lyot stop? 1 or 0
    lyot_x_shift_pupdiam = 0  # X,Y shear of Lyot stop mask; normalized relative to pupil diameter
    lyot_y_shift_pupdiam = 0
    lyot_x_shift_m = 0  # X,Y shear of Lyot stop mask in meters
    lyot_y_shift_m = 0
    use_field_stop = 1  # use field stop (HLC)? 1 or 0
    field_stop_radius_lam0 = 0  # field stop radius in lambda0/D (HLC or SPC-wide mask only)
    field_stop_x_offset = 0  # field stop offset in lambda0/D
    field_stop_y_offset = 0
    field_stop_x_offset_m = 0  # field stop offset in meters
    field_stop_y_offset_m = 0
    use_pupil_lens = 0  # use pupil imaging lens? 0 or 1
    use_defocus_lens = 0  # use defocusing lens? Options are 1, 2, 3, 4, corresponding to +18.0, +9.0, -4.0, -8.0 waves P-V @ 550 nm
    defocus = 0  # instead of specific lens, defocus in waves P-V @ 550 nm (-8.7 to 42.0 waves)
    final_sampling_m = 0  # final sampling in meters (overrides final_sampling_lam0)
    final_sampling_lam0 = 0  # final sampling in lambda0/D
    output_dim = output_dim0  # dimension of output in pixels (overrides output_dim0)

    if 'PASSVALUE' in locals():
        if 'use_fpm' in PASSVALUE: use_fpm = PASSVALUE['use_fpm']
        if 'cor_type' in PASSVALUE: cor_type = PASSVALUE['cor_type']

    is_spc = False
    is_hlc = False

    if cor_type == 'hlc':
        is_hlc = True
        file_directory = data_dir + '/hlc_20190210/'  # must have trailing "/"
        prefix = file_directory + 'run461_'
        pupil_diam_pix = 309.0
        pupil_file = prefix + 'pupil_rotated.fits'
        lyot_stop_file = prefix + 'lyot.fits'
        lambda0_m = 0.575e-6
        lam_occ = [
            5.4625e-07, 5.49444444444e-07, 5.52638888889e-07, 5.534375e-07,
            5.55833333333e-07, 5.59027777778e-07, 5.60625e-07,
            5.62222222222e-07, 5.65416666667e-07, 5.678125e-07,
            5.68611111111e-07, 5.71805555556e-07, 5.75e-07, 5.78194444444e-07,
            5.81388888889e-07, 5.821875e-07, 5.84583333333e-07,
            5.87777777778e-07, 5.89375e-07, 5.90972222222e-07,
            5.94166666667e-07, 5.965625e-07, 5.97361111111e-07,
            6.00555555556e-07, 6.0375e-07
        ]
        lam_occs = [
            '5.4625e-07', '5.49444444444e-07', '5.52638888889e-07',
            '5.534375e-07', '5.55833333333e-07', '5.59027777778e-07',
            '5.60625e-07', '5.62222222222e-07', '5.65416666667e-07',
            '5.678125e-07', '5.68611111111e-07', '5.71805555556e-07',
            '5.75e-07', '5.78194444444e-07', '5.81388888889e-07',
            '5.821875e-07', '5.84583333333e-07', '5.87777777778e-07',
            '5.89375e-07', '5.90972222222e-07', '5.94166666667e-07',
            '5.965625e-07', '5.97361111111e-07', '6.00555555556e-07',
            '6.0375e-07'
        ]
        lam_occs = [
            prefix + 'occ_lam' + s + 'theta6.69polp_' for s in lam_occs
        ]
        # find nearest matching FPM wavelength
        wlam = (np.abs(lambda_m - np.array(lam_occ))).argmin()
        occulter_file_r = lam_occs[wlam] + 'real.fits'
        occulter_file_i = lam_occs[wlam] + 'imag.fits'
        n_default = 1024  # gridsize in non-critical areas
        if use_fpm == 1:
            n_to_fpm = 2048
        else:
            n_to_fpm = 1024
        n_from_lyotstop = 1024
        field_stop_radius_lam0 = 9.0
    elif cor_type == 'hlc_erkin':
        is_hlc = True
        file_directory = data_dir + '/hlc_20190206_v3/'  # must have trailing "/"
        prefix = file_directory + 'dsn17d_run2_pup310_fpm2048_'
        pupil_diam_pix = 310.0
        pupil_file = prefix + 'pupil.fits'
        lyot_stop_file = prefix + 'lyot.fits'
        lambda0_m = 0.575e-6
        lam_occ = [
            5.4625e-07, 5.4944e-07, 5.5264e-07, 5.5583e-07, 5.5903e-07,
            5.6222e-07, 5.6542e-07, 5.6861e-07, 5.7181e-07, 5.75e-07,
            5.7819e-07, 5.8139e-07, 5.8458e-07, 5.8778e-07, 5.9097e-07,
            5.9417e-07, 5.9736e-07, 6.0056e-07, 6.0375e-07
        ]
        lam_occs = [
            '5.4625e-07', '5.4944e-07', '5.5264e-07', '5.5583e-07',
            '5.5903e-07', '5.6222e-07', '5.6542e-07', '5.6861e-07',
            '5.7181e-07', '5.75e-07', '5.7819e-07', '5.8139e-07', '5.8458e-07',
            '5.8778e-07', '5.9097e-07', '5.9417e-07', '5.9736e-07',
            '6.0056e-07', '6.0375e-07'
        ]
        lam_occs = [
            prefix + 'occ_lam' + s + 'theta6.69pols_' for s in lam_occs
        ]
        # find nearest matching FPM wavelength
        wlam = (np.abs(lambda_m - np.array(lam_occ))).argmin()
        occulter_file_r = lam_occs[wlam] + 'real_rotated.fits'
        occulter_file_i = lam_occs[wlam] + 'imag_rotated.fits'
        n_default = 1024  # gridsize in non-critical areas
        if use_fpm == 1:
            n_to_fpm = 2048
        else:
            n_to_fpm = 1024
        n_from_lyotstop = 1024
        field_stop_radius_lam0 = 9.0
    elif cor_type == 'spc-ifs_short' or cor_type == 'spc-ifs_long' or cor_type == 'spc-spec_short' or cor_type == 'spc-spec_long':
        is_spc = True
        file_dir = data_dir + '/spc_20190130/'  # must have trailing "/"
        pupil_diam_pix = 1000.0
        pupil_file = file_dir + 'pupil_SPC-20190130_rotated.fits'
        pupil_mask_file = file_dir + 'SPM_SPC-20190130.fits'
        fpm_file = file_dir + 'fpm_0.05lamdivD.fits'
        fpm_sampling = 0.05  # sampling in fpm_sampling_lambda_m/D of FPM mask
        if cor_type == 'spc-ifs_short' or cor_type == 'spc-spec_short':
            fpm_sampling_lambda_m = 0.66e-6
            lambda0_m = 0.66e-6
        else:
            fpm_sampling_lambda_m = 0.73e-6
            lambda0_m = 0.73e-6  # FPM scaled for this central wavelength
        lyot_stop_file = file_dir + 'LS_SPC-20190130.fits'
        n_default = 2048  # gridsize in non-critical areas
        n_to_fpm = 2048  # gridsize to/from FPM
        n_mft = 1400  # gridsize to FPM (propagation to/from FPM handled by MFT)
        n_from_lyotstop = 4096
    elif cor_type == 'spc-wide':
        is_spc = True
        file_dir = data_dir + '/spc_20181220/'  # must have trailing "/"
        pupil_diam_pix = 1000.0
        pupil_file = file_dir + 'pupil_SPC-20181220_1k_rotated.fits'
        pupil_mask_file = file_dir + 'SPM_SPC-20181220_1000_rounded9_gray.fits'
        fpm_file = file_dir + 'fpm_0.05lamdivD.fits'
        fpm_sampling = 0.05  # sampling in lambda0/D of FPM mask
        fpm_sampling_lambda_m = 0.825e-6
        lambda0_m = 0.825e-6  # FPM scaled for this central wavelength
        lyot_stop_file = file_dir + 'LS_SPC-20181220_1k.fits'
        n_default = 2048  # gridsize in non-critical areas
        n_to_fpm = 2048  # gridsize to/from FPM
        n_mft = 1400
        n_from_lyotstop = 4096
    elif cor_type == 'none':
        file_directory = data_dir + '/hlc_20190210/'  # must have trailing "/"
        prefix = file_directory + 'run461_'
        pupil_diam_pix = 309.0
        pupil_file = prefix + 'pupil_rotated.fits'
        lambda0_m = 0.575e-6
        use_fpm = 0
        use_lyot_stop = 0
        use_field_stop = 0
        n_default = 1024
        n_to_fpm = 1024
        n_from_lyotstop = 1024
    else:
        raise Exception('ERROR: Unsupported cor_type: ' + cor_type)

    if 'PASSVALUE' in locals():
        if 'lam0' in PASSVALUE: lamba0_m = PASSVALUE['lam0'] * 1.0e-6
        if 'lambda0_m' in PASSVALUE: lambda0_m = PASSVALUE['lambda0_m']
        mas_per_lamD = lambda0_m * 360.0 * 3600.0 / (
            2 * np.pi * 2.363) * 1000  # mas per lambda0/D
        if 'source_x_offset' in PASSVALUE:
            source_x_offset = PASSVALUE['source_x_offset']
        if 'source_y_offset' in PASSVALUE:
            source_y_offset = PASSVALUE['source_y_offset']
        if 'source_x_offset_mas' in PASSVALUE:
            source_x_offset = PASSVALUE['source_x_offset_mas'] / mas_per_lamD
        if 'source_y_offset_mas' in PASSVALUE:
            source_y_offset = PASSVALUE['source_y_offset_mas'] / mas_per_lamD
        if 'use_errors' in PASSVALUE: use_errors = PASSVALUE['use_errors']
        if 'polaxis' in PASSVALUE: polaxis = PASSVALUE['polaxis']
        if 'zindex' in PASSVALUE: zindex = np.array(PASSVALUE['zindex'])
        if 'zval_m' in PASSVALUE: zval_m = np.array(PASSVALUE['zval_m'])
        if 'end_at_fsm' in PASSVALUE: end_at_fsm = PASSVALUE['end_at_fsm']
        if 'cgi_x_shift_pupdiam' in PASSVALUE:
            cgi_x_shift_pupdiam = PASSVALUE['cgi_x_shift_pupdiam']
        if 'cgi_y_shift_pupdiam' in PASSVALUE:
            cgi_y_shift_pupdiam = PASSVALUE['cgi_y_shift_pupdiam']
        if 'cgi_x_shift_m' in PASSVALUE:
            cgi_x_shift_m = PASSVALUE['cgi_x_shift_m']
        if 'cgi_y_shift_m' in PASSVALUE:
            cgi_y_shift_m = PASSVALUE['cgi_y_shift_m']
        if 'fsm_x_offset' in PASSVALUE:
            fsm_x_offset = PASSVALUE['fsm_x_offset']
        if 'fsm_y_offset' in PASSVALUE:
            fsm_y_offset = PASSVALUE['fsm_y_offset']
        if 'fsm_x_offset_mas' in PASSVALUE:
            fsm_x_offset = PASSVALUE['fsm_x_offset_mas'] / mas_per_lamD
        if 'fsm_y_offset_mas' in PASSVALUE:
            fsm_y_offset = PASSVALUE['fsm_y_offset_mas'] / mas_per_lamD
        if 'focm_z_shift_m' in PASSVALUE:
            focm_z_shift_m = PASSVALUE['focm_z_shift_m']
        if 'use_hlc_dm_patterns' in PASSVALUE:
            use_hlc_dm_patterns = PASSVALUE['use_hlc_dm_patterns']
        if 'use_dm1' in PASSVALUE: use_dm1 = PASSVALUE['use_dm1']
        if 'dm1_m' in PASSVALUE: dm1_m = PASSVALUE['dm1_m']
        if 'dm1_xc_act' in PASSVALUE: dm1_xc_act = PASSVALUE['dm1_xc_act']
        if 'dm1_yc_act' in PASSVALUE: dm1_yc_act = PASSVALUE['dm1_yc_act']
        if 'dm1_xtilt_deg' in PASSVALUE:
            dm1_xtilt_deg = PASSVALUE['dm1_xtilt_deg']
        if 'dm1_ytilt_deg' in PASSVALUE:
            dm1_ytilt_deg = PASSVALUE['dm1_ytilt_deg']
        if 'dm1_ztilt_deg' in PASSVALUE:
            dm1_ztilt_deg = PASSVALUE['dm1_ztilt_deg']
        if 'use_dm2' in PASSVALUE: use_dm2 = PASSVALUE['use_dm2']
        if 'dm2_m' in PASSVALUE: dm2_m = PASSVALUE['dm2_m']
        if 'dm2_xc_act' in PASSVALUE: dm2_xc_act = PASSVALUE['dm2_xc_act']
        if 'dm2_yc_act' in PASSVALUE: dm2_yc_act = PASSVALUE['dm2_yc_act']
        if 'dm2_xtilt_deg' in PASSVALUE:
            dm2_xtilt_deg = PASSVALUE['dm2_xtilt_deg']
        if 'dm2_ytilt_deg' in PASSVALUE:
            dm2_ytilt_deg = PASSVALUE['dm2_ytilt_deg']
        if 'dm2_ztilt_deg' in PASSVALUE:
            dm2_ztilt_deg = PASSVALUE['dm2_ztilt_deg']
        if 'use_pupil_mask' in PASSVALUE:
            use_pupil_mask = PASSVALUE['use_pupil_mask']
        if 'mask_x_shift_pupdiam' in PASSVALUE:
            mask_x_shift_pupdiam = PASSVALUE['mask_x_shift_pupdiam']
        if 'mask_y_shift_pupdiam' in PASSVALUE:
            mask_y_shift_pupdiam = PASSVALUE['mask_y_shift_pupdiam']
        if 'mask_x_shift_m' in PASSVALUE:
            mask_x_shift_m = PASSVALUE['mask_x_shift_m']
        if 'mask_y_shift_m' in PASSVALUE:
            mask_y_shift_m = PASSVALUE['mask_y_shift_m']
        if 'fpm_x_offset' in PASSVALUE:
            fpm_x_offset = PASSVALUE['fpm_x_offset']
        if 'fpm_y_offset' in PASSVALUE:
            fpm_y_offset = PASSVALUE['fpm_y_offset']
        if 'fpm_x_offset_m' in PASSVALUE:
            fpm_x_offset_m = PASSVALUE['fpm_x_offset_m']
        if 'fpm_y_offset_m' in PASSVALUE:
            fpm_y_offset_m = PASSVALUE['fpm_y_offset_m']
        if 'fpm_z_shift_m' in PASSVALUE:
            fpm_z_shift_m = PASSVALUE['fpm_z_shift_m']
        if 'pinhole_diam_m' in PASSVALUE:
            pinhole_diam_m = PASSVALUE['pinhole_diam_m']
        if 'end_at_fpm_exit_pupil' in PASSVALUE:
            end_at_fpm_exit_pupil = PASSVALUE['end_at_fpm_exit_pupil']
        if 'output_field_rootname' in PASSVALUE:
            output_field_rootname = PASSVALUE['output_field_rootname']
        if 'use_lyot_stop' in PASSVALUE:
            use_lyot_stop = PASSVALUE['use_lyot_stop']
        if 'lyot_x_shift_pupdiam' in PASSVALUE:
            lyot_x_shift_pupdiam = PASSVALUE['lyot_x_shift_pupdiam']
        if 'lyot_y_shift_pupdiam' in PASSVALUE:
            lyot_y_shift_pupdiam = PASSVALUE['lyot_y_shift_pupdiam']
        if 'lyot_x_shift_m' in PASSVALUE:
            lyot_x_shift_m = PASSVALUE['lyot_x_shift_m']
        if 'lyot_y_shift_m' in PASSVALUE:
            lyot_y_shift_m = PASSVALUE['lyot_y_shift_m']
        if 'use_field_stop' in PASSVALUE:
            use_field_stop = PASSVALUE['use_field_stop']
        if 'field_stop_x_offset' in PASSVALUE:
            field_stop_x_offset = PASSVALUE['field_stop_x_offset']
        if 'field_stop_y_offset' in PASSVALUE:
            field_stop_y_offset = PASSVALUE['field_stop_y_offset']
        if 'field_stop_x_offset_m' in PASSVALUE:
            field_stop_x_offset_m = PASSVALUE['field_stop_x_offset_m']
        if 'field_stop_y_offset_m' in PASSVALUE:
            field_stop_y_offset_m = PASSVALUE['field_stop_y_offset_m']
        if 'use_pupil_lens' in PASSVALUE:
            use_pupil_lens = PASSVALUE['use_pupil_lens']
        if 'use_defocus_lens' in PASSVALUE:
            use_defocus_lens = PASSVALUE['use_defocus_lens']
        if 'defocus' in PASSVALUE: defocus = PASSVALUE['defocus']
        if 'output_dim' in PASSVALUE: output_dim = PASSVALUE['output_dim']
        if 'final_sampling_m' in PASSVALUE:
            final_sampling_m = PASSVALUE['final_sampling_m']
        if 'final_sampling_lam0' in PASSVALUE:
            final_sampling_lam0 = PASSVALUE['final_sampling_lam0']

    diam = 2.3633372
    fl_pri = 2.83459423440 * 1.0013
    d_pri_sec = 2.285150515460035
    d_focus_sec = d_pri_sec - fl_pri
    fl_sec = -0.653933011 * 1.0004095
    d_sec_focus = 3.580188916677103
    diam_sec = 0.58166
    d_sec_fold1 = 2.993753476654728
    d_fold1_focus = 0.586435440022375
    diam_fold1 = 0.09
    d_fold1_m3 = 1.680935841598811
    fl_m3 = 0.430216463069001
    d_focus_m3 = 1.094500401576436
    d_m3_pupil = 0.469156807701977
    d_m3_focus = 0.708841602661368
    diam_m3 = 0.2
    d_m3_m4 = 0.943514749358944
    fl_m4 = 0.116239114833590
    d_focus_m4 = 0.234673014520402
    d_m4_pupil = 0.474357941656967
    d_m4_focus = 0.230324117970585
    diam_m4 = 0.07
    d_m4_m5 = 0.429145636743193
    d_m5_focus = 0.198821518772608
    fl_m5 = 0.198821518772608
    d_m5_pupil = 0.716529242882632
    diam_m5 = 0.07
    d_m5_fold2 = 0.351125431220770
    diam_fold2 = 0.06
    d_fold2_fsm = 0.365403811661862
    d_fsm_oap1 = 0.354826767220001
    fl_oap1 = 0.503331895563883
    diam_oap1 = 0.06
    d_oap1_focm = 0.768005607094041
    d_focm_oap2 = 0.314483210543378
    fl_oap2 = 0.579156922073536
    diam_oap2 = 0.06
    d_oap2_dm1 = 0.775775726154228
    d_dm1_dm2 = 1.0
    d_dm2_oap3 = 0.394833855161549
    fl_oap3 = 1.217276467668519
    diam_oap3 = 0.06
    d_oap3_fold3 = 0.505329955078121
    diam_fold3 = 0.06
    d_fold3_oap4 = 1.158897671642761
    fl_oap4 = 0.446951159052363
    diam_oap4 = 0.06
    d_oap4_pupilmask = 0.423013568764728
    d_pupilmask_oap5 = 0.408810648253099
    fl_oap5 = 0.548189351937178
    diam_oap5 = 0.06
    d_oap5_fpm = 0.548189083164429
    d_fpm_oap6 = 0.548189083164429
    fl_oap6 = 0.548189083164429
    diam_oap6 = 0.06
    d_oap6_lyotstop = 0.687567667550736
    d_lyotstop_oap7 = 0.401748843470518
    fl_oap7 = 0.708251083480054
    diam_oap7 = 0.06
    d_oap7_fieldstop = 0.708251083480054
    d_fieldstop_oap8 = 0.210985967281651
    fl_oap8 = 0.210985967281651
    diam_oap8 = 0.06
    d_oap8_pupil = 0.238185804200797
    d_oap8_filter = 0.368452268225530
    diam_filter = 0.01
    d_filter_lens = 0.170799548215162
    fl_lens = 0.246017378417573 + 0.050001306014153
    diam_lens = 0.01
    d_lens_fold4 = 0.246017378417573
    diam_fold4 = 0.02
    d_fold4_image = 0.050001578514650
    fl_pupillens = 0.149260576823040

    n = n_default  # start off with less padding

    wavefront = proper.prop_begin(diam, lambda_m, n, float(pupil_diam_pix) / n)
    pupil = proper.prop_fits_read(pupil_file)
    proper.prop_multiply(wavefront, trim(pupil, n))
    pupil = 0
    if polaxis != 0: polmap(wavefront, polfile, pupil_diam_pix, polaxis)
    proper.prop_define_entrance(wavefront)
    proper.prop_lens(wavefront, fl_pri)
    if source_x_offset != 0 or source_y_offset != 0:
        # compute tilted wavefront to offset source by xoffset,yoffset lambda0_m/D
        xtilt_lam = -source_x_offset * lambda0_m / lambda_m
        ytilt_lam = -source_y_offset * lambda0_m / lambda_m
        x = np.tile((np.arange(n) - n // 2) / (pupil_diam_pix / 2.0), (n, 1))
        y = np.transpose(x)
        proper.prop_multiply(
            wavefront,
            np.exp(complex(0, 1) * np.pi * (xtilt_lam * x + ytilt_lam * y)))
        x = 0
        y = 0
    if zindex[0] != 0: proper.prop_zernikes(wavefront, zindex, zval_m)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_PRIMARY_phase_error_V1.0.fits',
                             WAVEFRONT=True)
        proper.prop_errormap(
            wavefront,
            map_dir +
            'wfirst_phaseb_GROUND_TO_ORBIT_4.2X_phase_error_V1.0.fits',
            WAVEFRONT=True)

    proper.prop_propagate(wavefront, d_pri_sec, 'secondary')
    proper.prop_lens(wavefront, fl_sec)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_SECONDARY_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_sec / 2.0)

    proper.prop_propagate(wavefront, d_sec_fold1, 'FOLD_1')
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_FOLD1_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_fold1 / 2.0)

    proper.prop_propagate(wavefront, d_fold1_m3, 'M3')
    proper.prop_lens(wavefront, fl_m3)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_M3_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_m3 / 2.0)

    proper.prop_propagate(wavefront, d_m3_m4, 'M4')
    proper.prop_lens(wavefront, fl_m4)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_M4_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_m4 / 2.0)

    proper.prop_propagate(wavefront, d_m4_m5, 'M5')
    proper.prop_lens(wavefront, fl_m5)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_M5_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_m5 / 2.0)

    proper.prop_propagate(wavefront, d_m5_fold2, 'FOLD_2')
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_FOLD2_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_fold2 / 2.0)

    proper.prop_propagate(wavefront, d_fold2_fsm, 'FSM')
    if end_at_fsm == 1:
        (wavefront, sampling_m) = proper.prop_end(wavefront, NOABS=True)
        wavefront = trim(wavefront, n)
        return wavefront, sampling_m
    if cgi_x_shift_pupdiam != 0 or cgi_y_shift_pupdiam != 0 or cgi_x_shift_m != 0 or cgi_y_shift_m != 0:  # bulk coronagraph pupil shear
        # FFT the field, apply a tilt, FFT back
        if cgi_x_shift_pupdiam != 0 or cgi_y_shift_pupdiam != 0:
            # offsets are normalized to pupil diameter
            xt = -cgi_x_shift_pupdiam * pupil_diam_pix * float(
                pupil_diam_pix) / n
            yt = -cgi_y_shift_pupdiam * pupil_diam_pix * float(
                pupil_diam_pix) / n
        else:
            # offsets are meters
            d_m = proper.prop_get_sampling(wavefront)
            xt = -cgi_x_shift_m / d_m * float(pupil_diam_pix) / n
            yt = -cgi_y_shift_m / d_m * float(pupil_diam_pix) / n
        x = np.tile((np.arange(n) - n // 2) / (pupil_diam_pix / 2.0), (n, 1))
        y = np.transpose(x)
        tilt = complex(0, 1) * np.pi * (x * xt + y * yt)
        x = 0
        y = 0
        wavefront0 = proper.prop_get_wavefront(wavefront)
        wavefront0 = ffts(wavefront0, -1)
        wavefront0 *= np.exp(tilt)
        wavefront0 = ffts(wavefront0, 1)
        tilt = 0
        wavefront.wfarr[:, :] = proper.prop_shift_center(wavefront0)
        wavefront0 = 0
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_FSM_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_fsm / 2.0)
    if (fsm_x_offset != 0.0 or fsm_y_offset != 0.0):
        # compute tilted wavefront to offset source by fsm_x_offset,fsm_y_offset lambda0_m/D
        xtilt_lam = fsm_x_offset * lambda0_m / lambda_m
        ytilt_lam = fsm_y_offset * lambda0_m / lambda_m
        x = np.tile((np.arange(n) - n // 2) / (pupil_diam_pix / 2.0), (n, 1))
        y = np.transpose(x)
        proper.prop_multiply(
            wavefront,
            np.exp(complex(0, 1) * np.pi * (xtilt_lam * x + ytilt_lam * y)))
        x = 0
        y = 0

    proper.prop_propagate(wavefront, d_fsm_oap1, 'OAP1')
    proper.prop_lens(wavefront, fl_oap1)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_OAP1_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_oap1 / 2.0)

    proper.prop_propagate(wavefront, d_oap1_focm + focm_z_shift_m, 'FOCM')
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_FOCM_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_focm / 2.0)

    proper.prop_propagate(wavefront, d_focm_oap2 + focm_z_shift_m, 'OAP2')
    proper.prop_lens(wavefront, fl_oap2)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_OAP2_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_oap2 / 2.0)

    proper.prop_propagate(wavefront, d_oap2_dm1, 'DM1')
    if use_dm1 != 0:
        proper.prop_dm(wavefront,
                       dm1_m,
                       dm1_xc_act,
                       dm1_yc_act,
                       dm_sampling_m,
                       XTILT=dm1_xtilt_deg,
                       YTILT=dm1_ytilt_deg,
                       ZTILT=dm1_ztilt_deg)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_DM1_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if is_hlc == True and use_hlc_dm_patterns == 1:
        dm1wfe = proper.prop_fits_read(prefix + 'dm1wfe.fits')
        proper.prop_add_phase(wavefront, trim(dm1wfe, n))
        dm1wfe = 0

    proper.prop_propagate(wavefront, d_dm1_dm2, 'DM2')
    if use_dm2 == 1:
        proper.prop_dm(wavefront,
                       dm2_m,
                       dm2_xc_act,
                       dm2_yc_act,
                       dm_sampling_m,
                       XTILT=dm2_xtilt_deg,
                       YTILT=dm2_ytilt_deg,
                       ZTILT=dm2_ztilt_deg)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_DM2_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if is_hlc == True:
        if use_hlc_dm_patterns == 1:
            dm2wfe = proper.prop_fits_read(prefix + 'dm2wfe.fits')
            proper.prop_add_phase(wavefront, trim(dm2wfe, n))
            dm2wfe = 0
        dm2mask = proper.prop_fits_read(prefix + 'dm2mask.fits')
        proper.prop_multiply(wavefront, trim(dm2mask, n))
        dm2mask = 0

    proper.prop_propagate(wavefront, d_dm2_oap3, 'OAP3')
    proper.prop_lens(wavefront, fl_oap3)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_OAP3_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_oap3 / 2.0)

    proper.prop_propagate(wavefront, d_oap3_fold3, 'FOLD_3')
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_FOLD3_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_fold3 / 2.0)

    proper.prop_propagate(wavefront, d_fold3_oap4, 'OAP4')
    proper.prop_lens(wavefront, fl_oap4)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_OAP4_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_oap4 / 2.0)

    proper.prop_propagate(wavefront, d_oap4_pupilmask,
                          'PUPIL_MASK')  # flat/reflective shaped pupil
    if is_spc == True and use_pupil_mask != 0:
        pupil_mask = proper.prop_fits_read(pupil_mask_file)
        pupil_mask = trim(pupil_mask, n)
        if mask_x_shift_pupdiam != 0 or mask_y_shift_pupdiam != 0 or mask_x_shift_m != 0 or mask_y_shift_m != 0:
            # shift SP mask by FFTing it, applying tilt, and FFTing back
            if mask_x_shift_pupdiam != 0 or mask_y_shift_pupdiam != 0:
                # offsets are normalized to pupil diameter
                xt = -mask_x_shift_pupdiam * pupil_diam_pix * float(
                    pupil_diam_pix) / n
                yt = -mask_y_shift_pupdiam * pupil_diam_pix * float(
                    pupil_diam_pix) / n
            else:
                d_m = proper.prop_get_sampling(wavefront)
                xt = -mask_x_shift_m / d_m * float(pupil_diam_pix) / n
                yt = -mask_y_shift_m / d_m * float(pupil_diam_pix) / n
            x = np.tile((np.arange(n) - n // 2) / (pupil_diam_pix / 2.0),
                        (n, 1))
            y = np.transpose(x)
            tilt = complex(0, 1) * np.pi * (x * xt + y * yt)
            x = 0
            y = 0
            pupil_mask = ffts(pupil_mask, -1)
            pupil_mask *= np.exp(tilt)
            pupil_mask = ffts(pupil_mask, 1)
            pupil_mask = pupil_mask.real
            tilt = 0
        proper.prop_multiply(wavefront, pupil_mask)
        pupil_mask = 0
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_PUPILMASK_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    # while at a pupil, use more padding to provide 2x better sampling at FPM
    diam = 2 * proper.prop_get_beamradius(wavefront)
    (wavefront, dx) = proper.prop_end(wavefront, NOABS=True)
    n = n_to_fpm
    wavefront0 = trim(wavefront, n)
    wavefront = proper.prop_begin(diam, lambda_m, n, float(pupil_diam_pix) / n)
    wavefront.wfarr[:, :] = proper.prop_shift_center(wavefront0)
    wavefront0 = 0

    proper.prop_propagate(wavefront, d_pupilmask_oap5, 'OAP5')
    proper.prop_lens(wavefront, fl_oap5)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_OAP5_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_oap5 / 2.0)

    proper.prop_propagate(wavefront,
                          d_oap5_fpm + fpm_z_shift_m,
                          'FPM',
                          TO_PLANE=True)
    if use_fpm == 1:
        if fpm_x_offset != 0 or fpm_y_offset != 0 or fpm_x_offset_m != 0 or fpm_y_offset_m != 0:
            # To shift FPM, FFT field to pupil, apply tilt, FFT back to focus,
            # apply FPM, FFT to pupil, take out tilt, FFT back to focus
            if fpm_x_offset != 0 or fpm_y_offset != 0:
                # shifts are specified in lambda0/D
                x_offset_lamD = fpm_x_offset * lambda0_m / lambda_m
                y_offset_lamD = fpm_y_offset * lambda0_m / lambda_m
            else:
                d_m = proper.prop_get_sampling(wavefront)
                x_offset_lamD = fpm_x_offset_m / d_m * float(
                    pupil_diam_pix) / n
                y_offset_lamD = fpm_y_offset_m / d_m * float(
                    pupil_diam_pix) / n
            x = np.tile((np.arange(n) - n // 2) / (pupil_diam_pix / 2.0),
                        (n, 1))
            y = np.transpose(x)
            tilt = complex(0,
                           1) * np.pi * (x * x_offset_lamD + y * y_offset_lamD)
            x = 0
            y = 0
            wavefront0 = proper.prop_get_wavefront(wavefront)
            wavefront0 = ffts(wavefront0, -1)
            wavefront0 *= np.exp(tilt)
            wavefront0 = ffts(wavefront0, 1)
            wavefront.wfarr[:, :] = proper.prop_shift_center(wavefront0)
            wavefront0 = 0
        if is_hlc == True:
            occ_r = proper.prop_fits_read(occulter_file_r)
            occ_i = proper.prop_fits_read(occulter_file_i)
            occ = np.array(occ_r + 1j * occ_i, dtype=np.complex128)
            proper.prop_multiply(wavefront, trim(occ, n))
            occ_r = 0
            occ_i = 0
            occ = 0
        elif is_spc == True:
            # super-sample FPM
            wavefront0 = proper.prop_get_wavefront(wavefront)
            wavefront0 = ffts(wavefront0, 1)  # to virtual pupil
            wavefront0 = trim(wavefront0, n_mft)
            fpm = proper.prop_fits_read(fpm_file)
            nfpm = fpm.shape[1]
            fpm_sampling_lam = fpm_sampling * fpm_sampling_lambda_m / lambda_m
            wavefront0 = mft2(wavefront0, fpm_sampling_lam, pupil_diam_pix,
                              nfpm, -1)  # MFT to highly-sampled focal plane
            wavefront0 *= fpm
            fpm = 0
            wavefront0 = mft2(wavefront0, fpm_sampling_lam, pupil_diam_pix, n,
                              +1)  # MFT to virtual pupil
            wavefront0 = ffts(wavefront0,
                              -1)  # back to normally-sampled focal plane
            wavefront.wfarr[:, :] = proper.prop_shift_center(wavefront0)
            wavefront0 = 0
        if fpm_x_offset != 0 or fpm_y_offset != 0 or fpm_x_offset_m != 0 or fpm_y_offset_m != 0:
            wavefront0 = proper.prop_get_wavefront(wavefront)
            wavefront0 = ffts(wavefront0, -1)
            wavefront0 *= np.exp(-tilt)
            wavefront0 = ffts(wavefront0, 1)
            wavefront.wfarr[:, :] = proper.prop_shift_center(wavefront0)
            wavefront0 = 0
            tilt = 0
    if pinhole_diam_m != 0:
        # "pinhole_diam_m" is pinhole diameter in meters
        dx_m = proper.prop_get_sampling(wavefront)
        dx_pinhole_diam_m = pinhole_diam_m / 101.0  # 101 samples across pinhole
        n_out = 105
        m_per_lamD = dx_m * n / float(
            pupil_diam_pix)  # current focal plane sampling in lambda_m/D
        dx_pinhole_lamD = dx_pinhole_diam_m / m_per_lamD  # pinhole sampling in lambda_m/D
        n_in = int(round(pupil_diam_pix * 1.2))
        wavefront0 = proper.prop_get_wavefront(wavefront)
        wavefront0 = ffts(wavefront0, +1)  # to virtual pupil
        wavefront0 = trim(wavefront0, n_in)
        m = dx_pinhole_lamD * n_in * float(n_out) / pupil_diam_pix
        wavefront0 = mft2(wavefront0, dx_pinhole_lamD, pupil_diam_pix, n_out,
                          -1)  # MFT to highly-sampled focal plane
        p = (radius(n_out) * dx_pinhole_diam_m) <= (pinhole_diam_m / 2.0)
        p = p.astype(np.int)
        wavefront0 *= p
        p = 0
        wavefront0 = mft2(wavefront0, dx_pinhole_lamD, pupil_diam_pix, n,
                          +1)  # MFT back to virtual pupil
        wavefront0 = ffts(wavefront0,
                          -1)  # back to normally-sampled focal plane
        wavefront.wfarr[:, :] = proper.prop_shift_center(wavefront0)
        wavefront0 = 0

    proper.prop_propagate(wavefront, d_fpm_oap6 - fpm_z_shift_m, 'OAP6')
    proper.prop_lens(wavefront, fl_oap6)
    if use_errors != 0 and end_at_fpm_exit_pupil == 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_OAP6_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_oap6 / 2.0)

    proper.prop_propagate(wavefront, d_oap6_lyotstop, 'LYOT_STOP')
    # while at a pupil, switch back to less padding
    diam = 2 * proper.prop_get_beamradius(wavefront)
    (wavefront, dx) = proper.prop_end(wavefront, NOABS=True)
    n = n_from_lyotstop
    wavefront = trim(wavefront, n)
    if output_field_rootname != '':
        lams = format(lambda_m * 1e6, "6.4f")
        pols = format(int(round(polaxis)))
        hdu = pyfits.PrimaryHDU()
        hdu.data = np.real(wavefront)
        hdu.writeto(output_field_rootname + '_' + lams + 'um_' + pols +
                    '_real.fits',
                    overwrite=True)
        hdu = pyfits.PrimaryHDU()
        hdu.data = np.imag(wavefront)
        hdu.writeto(output_field_rootname + '_' + lams + 'um_' + pols +
                    '_imag.fits',
                    overwrite=True)
    if end_at_fpm_exit_pupil == 1:
        return wavefront, dx
    wavefront0 = wavefront.copy()
    wavefront = 0
    wavefront = proper.prop_begin(diam, lambda_m, n, float(pupil_diam_pix) / n)
    wavefront.wfarr[:, :] = proper.prop_shift_center(wavefront0)
    wavefront0 = 0

    if use_lyot_stop != 0:
        lyot = proper.prop_fits_read(lyot_stop_file)
        lyot = trim(lyot, n)
        if lyot_x_shift_pupdiam != 0 or lyot_y_shift_pupdiam != 0 or lyot_x_shift_m != 0 or lyot_y_shift_m != 0:
            # apply shift to lyot stop by FFTing the stop, applying a tilt, and FFTing back
            if lyot_x_shift_pupdiam != 0 or lyot_y_shift_pupdiam != 0:
                # offsets are normalized to pupil diameter
                xt = -lyot_x_shift_pupdiam * pupil_diam_pix * float(
                    pupil_diam_pix) / n
                yt = -lyot_y_shift_pupdiam * pupil_diam_pix * float(
                    pupil_diam_pix) / n
            else:
                d_m = proper.prop_get_sampling(wavefront)
                xt = -lyot_x_shift_m / d_m * float(pupil_diam_pix) / n
                yt = -lyot_y_shift_m / d_m * float(pupil_diam_pix) / n
            x = np.tile((np.arange(n) - n // 2) / (pupil_diam_pix / 2.0),
                        (n, 1))
            y = np.transpose(x)
            tilt = complex(0, 1) * np.pi * (x * xt + y * yt)
            x = 0
            y = 0
            lyot = ffts(lyot, -1)
            lyot *= np.exp(tilt)
            lyot = ffts(lyot, 1)
            lyot = lyot.real
            tilt = 0
        proper.prop_multiply(wavefront, lyot)
        lyot = 0
    if use_pupil_lens != 0 or pinhole_diam_m != 0:
        proper.prop_circular_aperture(wavefront, 1.1, NORM=True)

    proper.prop_propagate(wavefront, d_lyotstop_oap7, 'OAP7')
    proper.prop_lens(wavefront, fl_oap7)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_OAP7_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_oap7 / 2.0)

    proper.prop_propagate(wavefront, d_oap7_fieldstop, 'FIELD_STOP')
    if use_field_stop != 0 and (cor_type == 'hlc' or cor_type == 'hlc_erkin'):
        sampling_lamD = float(
            pupil_diam_pix) / n  # sampling at focus in lambda_m/D
        stop_radius = field_stop_radius_lam0 / sampling_lamD * (
            lambda0_m / lambda_m) * proper.prop_get_sampling(wavefront)
        if field_stop_x_offset != 0 or field_stop_y_offset != 0:
            # convert offsets in lambda0/D to meters
            x_offset_lamD = field_stop_x_offset * lambda0_m / lambda_m
            y_offset_lamD = field_stop_y_offset * lambda0_m / lambda_m
            pupil_ratio = float(pupil_diam_pix) / n
            field_stop_x_offset_m = x_offset_lamD / pupil_ratio * proper.prop_get_sampling(
                wavefront)
            field_stop_y_offset_m = y_offset_lamD / pupil_ratio * proper.prop_get_sampling(
                wavefront)
        proper.prop_circular_aperture(wavefront, stop_radius,
                                      -field_stop_x_offset_m,
                                      -field_stop_y_offset_m)

    proper.prop_propagate(wavefront, d_fieldstop_oap8, 'OAP8')
    proper.prop_lens(wavefront, fl_oap8)
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_OAP8_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_oap8 / 2.0)

    proper.prop_propagate(wavefront, d_oap8_filter, 'filter')
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_FILTER_phase_error_V1.0.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_filter / 2.0)

    proper.prop_propagate(wavefront, d_filter_lens, 'LENS')
    if use_pupil_lens == 0 and use_defocus_lens == 0 and defocus == 0:
        # use imaging lens to create normal focus
        proper.prop_lens(wavefront, fl_lens)
        if use_errors != 0:
            proper.prop_errormap(wavefront,
                                 map_dir +
                                 'wfirst_phaseb_LENS_phase_error_V1.0.fits',
                                 WAVEFRONT=True)
    elif use_pupil_lens != 0:
        # use pupil imaging lens
        proper.prop_lens(wavefront, fl_pupillens)
        if use_errors != 0:
            proper.prop_errormap(
                wavefront,
                map_dir + 'wfirst_phaseb_PUPILLENS_phase_error_V1.0.fits',
                WAVEFRONT=True)
    else:
        # table is waves P-V @ 575 nm
        z4_pv_waves = np.array([
            -9.0545, -8.5543, -8.3550, -8.0300, -7.54500, -7.03350, -6.03300,
            -5.03300, -4.02000, -2.51980, 0.00000000, 3.028000, 4.95000,
            6.353600, 8.030000, 10.10500, 12.06000, 14.06000, 20.26000,
            28.34000, 40.77500, 56.65700
        ])
        fl_defocus_lens = np.array([
            5.09118, 1.89323, 1.54206, 1.21198, 0.914799, 0.743569, 0.567599,
            0.470213, 0.406973, 0.350755, 0.29601868, 0.260092, 0.24516,
            0.236606, 0.228181, 0.219748, 0.213278, 0.207816, 0.195536,
            0.185600, 0.176629, 0.169984
        ])
        # subtract ad-hoc function to make z4 vs f_length more accurately spline interpolatible
        f = fl_defocus_lens / 0.005
        f0 = 59.203738
        z4t = z4_pv_waves - (0.005 * (f0 - f - 40)) / f**2 / 0.575e-6
        if use_defocus_lens != 0:
            # use one of 4 defocusing lenses
            defocus = np.array([18.0, 9.0, -4.0, -8.0])  # waves P-V @ 575 nm
            f = interp1d(z4_pv_waves, z4t, kind='cubic')
            z4x = f(defocus)
            f = interp1d(z4t, fl_defocus_lens, kind='cubic')
            lens_fl = f(z4x)
            proper.prop_lens(wavefront, lens_fl[use_defocus_lens - 1])
            if use_errors != 0:
                proper.prop_errormap(wavefront,
                                     map_dir + 'wfirst_phaseb_DEFOCUSLENS' +
                                     str(use_defocus_lens) +
                                     '_phase_error_V1.0.fits',
                                     WAVEFRONT=True)
            defocus = defocus[use_defocus_lens - 1]
        else:
            # specify amount of defocus (P-V waves @ 575 nm)
            f = interp1d(z4_pv_waves, z4t, kind='cubic')
            z4x = f(defocus)
            f = interp1d(z4t, fl_defocus_lens, kind='cubic')
            lens_fl = f(z4x)
            proper.prop_lens(wavefront, lens_fl)
            if use_errors != 0:
                proper.prop_errormap(
                    wavefront,
                    map_dir +
                    'wfirst_phaseb_DEFOCUSLENS1_phase_error_V1.0.fits',
                    WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_lens / 2.0)

    proper.prop_propagate(wavefront, d_lens_fold4, 'FOLD_4')
    if use_errors != 0:
        proper.prop_errormap(wavefront,
                             map_dir +
                             'wfirst_phaseb_FOLD4_phase_error_V1.1.fits',
                             WAVEFRONT=True)
    if use_aperture != 0:
        proper.prop_circular_aperture(wavefront, diam_fold4 / 2.0)

    if defocus != 0 or use_defocus_lens != 0:
        if np.abs(defocus) <= 4:
            proper.prop_propagate(wavefront,
                                  d_fold4_image,
                                  'IMAGE',
                                  TO_PLANE=True)
        else:
            proper.prop_propagate(wavefront, d_fold4_image, 'IMAGE')
    else:
        proper.prop_propagate(wavefront, d_fold4_image, 'IMAGE')

    (wavefront, sampling_m) = proper.prop_end(wavefront, NOABS=True)

    if final_sampling_lam0 != 0 or final_sampling_m != 0:
        if final_sampling_m != 0:
            mag = sampling_m / final_sampling_m
            sampling_m = final_sampling_m
        else:
            mag = (float(pupil_diam_pix) /
                   n) / final_sampling_lam0 * (lambda_m / lambda0_m)
            sampling_m = sampling_m / mag
        wavefront = proper.prop_magnify(wavefront,
                                        mag,
                                        output_dim,
                                        AMP_CONSERVE=True)
    else:
        wavefront = trim(wavefront, output_dim)

    return wavefront, sampling_m
Example #6
0
def deformable_mirror(wf,
                      WFS_map,
                      iter,
                      previous_output=None,
                      apodize=False,
                      plane_name='',
                      debug=False):
    """
    combine different DM actuator commands into single map to send to prop_dm

    prop_dm needs an input map of n_actuators x n_actuators in units of actuator command height. quick_ao will handle
    the conversion to actuator command height, and the CDI probe must be scaled in cdi.probe_amp in params in
    units of m. Each subroutine is also responsible for creating a map of n_actuators x n_actuators spacing. prop_dm
    handles the resampling of this map onto the wavefront, including the influence function. Its some wizardry that
    happens in c, and presumably it is taken care of so you don't have to worry about it.

    In the call to proper.prop_dm, we apply the flag tp.fit_dm, which switches between two 'modes' of proper's DM
    surface fitting. If FALSE, the DM is driven to the heights specified by dm_map, and the influence function will
    act on these heights to define the final surface shape applied to the DM, which may differ substantially from
    the initial heights specified by dm_map. If TRUE, proper will iterate applying the influence function to the
    input heights, and adjust the heights until the difference between the influenced-map and input map meets some
    proper-defined convergence criterea. Setting tp.fit_dm=TRUE will obviously slow down the code, but will (likely)
    more accurately represent a well-calibrated DM response function.

    much of this code copied over from example from Proper manual on pg 94

    :param wf: single wavefront
    :param WFS_map: wavefront sensor map, should be in units of phase delay
    :param previous_output:
    :param iter: the current index of iteration (which timestep this is)
    :param plane_name: name of plane (should be 'woofer' or 'tweeter' for best functionality)
    :return: nothing is returned, but the probe map has been applied to the DM via proper.prop_dm. DM plane post DM
        application can be saved via the sp.save_list functionality
    """
    assert np.logical_xor(WFS_map is None, previous_output is None)

    # AO Actuator Count from DM Type
    if plane_name == 'tweeter' and hasattr(tp, 'act_tweeter'):
        nact = tp.act_tweeter
    elif plane_name == 'woofer' and hasattr(tp, 'act_woofer'):
        nact = tp.act_woofer
    else:
        nact = tp.ao_act

    # DM Coordinates
    nact_across_pupil = nact - 2  # number of full DM actuators across pupil (oversizing DM extent)
    dm_xc = (
        nact / 2
    )  # The location of the optical axis (center of the wavefront) on the DM in
    dm_yc = (
        nact / 2
    )  # actuator units. First actuator is centered on (0.0, 0.0). The 0.5 is a
    #  parameter introduced/tuned by Rupert to remove weird errors (address this).
    # KD verified this needs to be here or else suffer weird errors 9/19
    # TODO address/remove the 0.5 in DM x,y coordinates

    ############################
    # Creating DM Surface Map
    ############################
    d_beam = 2 * proper.prop_get_beamradius(wf)  # beam diameter
    act_spacing = d_beam / nact_across_pupil  # actuator spacing [m]

    #######
    # AO
    #######
    if previous_output is not None and WFS_map is None:
        dm_map = update_dm(previous_output)
    else:
        dm_map = quick_ao(wf, nact, WFS_map[wf.iw])

    #########
    # Waffle
    #########
    if tp.satelite_speck['apply'] and plane_name is not 'woofer':
        waffle = make_speckle_kxy(tp.satelite_speck['xloc'],
                                  tp.satelite_speck['yloc'],
                                  tp.satelite_speck['amp'],
                                  tp.satelite_speck['phase'])
        waffle += make_speckle_kxy(tp.satelite_speck['xloc'],
                                   -tp.satelite_speck['yloc'],
                                   tp.satelite_speck['amp'],
                                   tp.satelite_speck['phase'])
        dm_map += waffle

    #######
    # CDI
    ######
    if cdi.use_cdi and plane_name == cdi.which_DM:
        theta = cdi.phase_series[iter]
        if not np.isnan(theta):
            # dprint(f"Applying CDI probe, lambda = {wfo.wsamples[iw]*1e9:.2f} nm")
            cdi.save_tseries(iter, datetime.datetime.now())
            probe = config_probe(theta, nact, iw=wf.iw, ib=wf.ib, tstep=iter)
            dm_map = dm_map + probe  # Add Probe to DM map

    #########################
    # Applying Piston Error
    #########################
    if tp.piston_error:
        mean_dm_map = np.mean(np.abs(dm_map))
        var = 1e-4  # 1e-11
        dm_map = dm_map + np.random.normal(0, var,
                                           (dm_map.shape[0], dm_map.shape[1]))

    #########################
    # proper.prop_dm
    #########################
    dmap = proper.prop_dm(wf, dm_map, dm_xc, dm_yc, act_spacing,
                          FIT=tp.fit_dm)  #

    if debug and wf.iw == 0 and wf.ib == 0 and iter == 0:
        dprint(plane_name)
        check_sampling(wf,
                       iter,
                       plane_name + ' DM pupil plane',
                       getframeinfo(stack()[0][0]),
                       units='mm')

        quick2D(WFS_map[wf.iw],
                title=f"WFS map after masking",
                zlabel='unwrapped phase (rad)',
                vlim=[-3 * np.pi, 3 * np.pi])

        fig, ax = plt.subplots(1, 1)
        cax = ax.imshow(dm_map * 1e9, interpolation='none', origin='lower')
        plt.title(f'{plane_name} dm_map (actuator coordinates)')
        cb = plt.colorbar(cax)
        cb.set_label('nm')

        plt.show()

        post_ao = unwrap_phase(
            proper.prop_get_phase(wf)) * wf.lamda / (2 * np.pi)
        # quick2D(pre_ao_dist*1e9, title='unwrapped wavefront before DM', zlabel='nm', show=False)  # , vlim=(-0.5e-7,0.5e-7))
        # quick2D(np.abs(pre_ao_amp)**2, title='Pre-AO Intensity', show=False)#, vlim=(-0.5e-7,0.5e-7))
        # quick2D(dmap, title='the phase map prop_dm is applying', zlabel='distance (m)', show=False)#, vlim=(-0.5e-7,0.5e-7))
        # plt.figure()
        # plt.plot(pre_ao_dist[len(pre_ao_dist)//2], label=f'pre_ao 1D cut, row {len(pre_ao_dist)//2}')
        # plt.plot(2*dmap[len(dmap)//2], label=f'dmap 1D cut (x2), row {len(dmap)//2}')
        # plt.plot((pre_ao_dist + (2*dmap))[len(dmap)//2], label='difference')
        # plt.legend()
        # plt.xlim(sp.grid_size//2*np.array([1-sp.beam_ratio*1.1, 1+sp.beam_ratio*1.1]))
        # quick2D(pre_ao + (2*dmap), title='diff', zlabel='m', show=False, vlim=(-0.5e-7,0.5e-7))
        # quick2D(post_ao, title='unwrapped wavefront after DM', zlabel='m', show=True, vlim=(-0.5e-7,0.5e-7))
        # quick2D(np.abs(proper.prop_get_amplitude(wf))**2, title='wavefront after DM intensity', show=False)
        # quick2D(proper.prop_get_phase(wf), title='wavefront after DM in phase units', zlabel='Phase',
        #          show=True)  # colormap='sunlight',

    if apodize:
        hardmask_pupil(wf)

    return dmap
Example #7
0
def propcustom_dm(wf, dm_z0, dm_xc, dm_yc, spacing=0., **kwargs):
    """
    Generate a deformable mirror surface almost exactly like PROPER.

    Simulate a deformable mirror of specified actuator spacing, including the
    effects of the DM influence function. Has two more optional keywords
    compared to  proper.prop_dm

    Parameters
    ----------
    wf : obj
        WaveFront class object
    dm_z0 : str or numpy ndarray
        Either a 2D numpy array containing the surface piston of each DM
        actuator in meters or the name of a 2D FITS image file containing the
        above
    dm_xc, dm_yc : list or numpy ndarray
        The location of the optical axis (center of the wavefront) on the DM in
        actuator units (0 ro num_actuator-1). The center of the first actuator
        is (0.0, 0.0)
    spacing : float
        Defines the spacing in meters between actuators; must not be used when
        n_act_across_pupil is specified.

    Returns
    -------
    dmap : numpy ndarray
        Returns DM surface (not wavefront) map in meters

    Other Parameters
    ----------------
    FIT : bool
        Switch that tells routine that the values in "dm_z" are the desired
        surface heights rather than commanded actuator heights, and so the
        routine should fit this map, accounting for actuator influence functions,
        to determine the necessary actuator heights. An iterative error-minimizing
        loop is used for the fit.
    NO_APPLY : bool
        If set, the DM pattern is not added to the wavefront. Useful if the DM
        surface map is needed but should not be applied to the wavefront
    N_ACT_ACROSS_PUPIL : int
        Specifies the number of actuators that span the X-axis beam diameter. If
        it is a whole number, the left edge of the left pixel is aligned with
        the left edge of the beam, and the right edge of the right pixel with
        the right edge of the beam. This determines the spacing and size of the
        actuators. Should not be used when "spacing" value is specified.
    XTILT, YTILT, ZTILT : float
        Specify the rotation of the DM surface with respect to the wavefront plane
        in degrees about the X, Y, Z axes, respectively, with the origin at the
        center of the wavefront. The DM surface is interpolated and orthographically
        projected onto the wavefront grid. The coordinate system assumes that
        the wavefront and initial DM surface are in the X,Y plane with a lower
        left origin with Z towards the observer. The rotations are left handed.
        The default rotation order is X, Y, then Z unless the /ZYX switch is set.
    XYZ or ZYX : bool
        Specifies the rotation order if two or more of XTILT, YTILT, or ZTILT
        are specified. The default is /XYZ for X, Y, then Z rotations.
    inf_fn : string
        specify a new influence function as a FITS file with the same header keywords as 
        PROPER's default influence function. Needs these values in info.PrimaryData.Keywords:
            'P2PDX_M' % pixel width x (m)
            'P2PDY_M' % pixel width y (m)
            'C2CDX_M' % actuator pitch x (m)
            'C2CDY_M' % actuator pitch y (m)
    inf_sign : {+,-}
        specifies the sign (+/-) of the influence function. Given as an option because 
        the default influence function file is positive, but positive DM actuator 
        commands make a negative deformation for Xinetics and BMC DMs.

    Raises
    ------
    ValueError:
        User cannot specify both actuator spacing and N_ACT_ACROSS_PUPIL
    ValueError:
        User must specify either actuator spacing or N_ACT_ACROSS_PUPIL
    """
    if "ZYX" in kwargs and "XYZ" in kwargs:
        raise ValueError('PROP_DM: Error: Cannot specify both XYZ and ZYX ' +
                         'rotation orders. Stopping')
    elif "ZYX" not in kwargs and 'XYZ' not in kwargs:
        XYZ = 1  # default is rotation around X, then Y, then Z
        # ZYX = 0
    elif "ZYX" in kwargs:
        # ZYX = 1
        XYZ = 0
    elif "XYZ" in kwargs:
        XYZ = 1
        # ZYX = 0

    if "XTILT" in kwargs:
        xtilt = kwargs["XTILT"]
    else:
        xtilt = 0.

    if "YTILT" in kwargs:
        ytilt = kwargs["YTILT"]
    else:
        ytilt = 0.

    if "ZTILT" in kwargs:
        ztilt = kwargs["ZTILT"]
    else:
        ztilt = 0.

    if type(dm_z0) == str:
        dm_z = proper.prop_fits_read(dm_z0)  # Read DM setting from FITS file
    else:
        dm_z = dm_z0
    
    if "inf_fn" in kwargs:
        inf_fn = kwargs["inf_fn"]
    else:
        inf_fn = "influence_dm5v2.fits"
        
    if "inf_sign" in kwargs:
        if(kwargs["inf_sign"] == '+'):
            sign_factor = 1.
        elif(kwargs["inf_sign"] == '-'):
            sign_factor = -1.
    else:
        sign_factor = 1.

    n = proper.prop_get_gridsize(wf)
    dx_surf = proper.prop_get_sampling(wf)  # sampling of surface in meters
    beamradius = proper.prop_get_beamradius(wf)

    # Default influence function sampling is 0.1 mm, peak at (x,y)=(45,45)
    # Default influence function has shape = 1x91x91. Saving it as a 2D array
    # before continuing with processing
    dir_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                            "data")
    inf = proper.prop_fits_read(os.path.join(dir_path, inf_fn))
    inf = sign_factor*np.squeeze(inf)
    
    s = inf.shape
    nx_inf = s[1]
    ny_inf = s[0]
    xc_inf = nx_inf // 2
    yc_inf = ny_inf // 2
    dx_inf = 0.1e-3  # influence function spacing in meters
    dx_dm_inf = 1.0e-3  # nominal spacing between DM actuators in meters
    inf_mag = 10

    if spacing != 0 and "N_ACT_ACROSS_PUPIL" in kwargs:
        raise ValueError("PROP_DM: User cannot specify both actuator spacing" +
                         "and N_ACT_ACROSS_PUPIL. Stopping.")

    if spacing == 0 and "N_ACT_ACROSS_PUPIL" not in kwargs:
        raise ValueError("PROP_DM: User must specify either actuator spacing" +
                         " or N_ACT_ACROSS_PUPIL. Stopping.")

    if "N_ACT_ACROSS_PUPIL" in kwargs:
        dx_dm = 2. * beamradius / int(kwargs["N_ACT_ACROSS_PUPIL"])
    else:
        dx_dm = spacing

    dx_inf = dx_inf * dx_dm / dx_dm_inf  # Influence function sampling scaled
                                         # to specified DM actuator spacing

    if "FIT" in kwargs:
        x = (np.arange(5, dtype=np.float64) - 2) * dx_dm

        if proper.use_cubic_conv:
            inf_kernel = proper.prop_cubic_conv(inf.T, x/dx_inf+xc_inf,
                                                x/dx_inf+yc_inf, GRID=True)
        else:
            xygrid = np.meshgrid(x/dx_inf+xc_inf, x/dx_inf+yc_inf)
            inf_kernel = map_coordinates(inf.T, xygrid, order=3,
                                         mode="nearest")

        (dm_z_commanded, dms) = proper.prop_fit_dm(dm_z, inf_kernel)
    else:
        dm_z_commanded = dm_z

    s = dm_z.shape
    nx_dm = s[1]
    ny_dm = s[0]

    # Create subsampled DM grid
    margin = 9 * inf_mag
    nx_grid = nx_dm * inf_mag + 2 * margin
    ny_grid = ny_dm * inf_mag + 2 * margin
    xoff_grid = margin + inf_mag/2           # pixel location of 1st actuator center in subsampled grid
    yoff_grid = xoff_grid
    dm_grid = np.zeros([ny_grid, nx_grid], dtype = np.float64)

    x = np.arange(nx_dm, dtype=int) * int(inf_mag) + int(xoff_grid)
    y = np.arange(ny_dm, dtype=int) * int(inf_mag) + int(yoff_grid)
    dm_grid[np.tile(np.vstack(y), (nx_dm,)),
            np.tile(x, (ny_dm, 1))] = dm_z_commanded
    dm_grid = ss.fftconvolve(dm_grid, inf, mode='same')
    
    # 3D rotate DM grid and project orthogonally onto wavefront
    xdim = int(np.round(np.sqrt(2) * nx_grid * dx_inf / dx_surf)) # grid dimensions (pix) projected onto wavefront
    ydim = int(np.round(np.sqrt(2) * ny_grid * dx_inf / dx_surf))

    if xdim > n: xdim = n

    if ydim > n: ydim = n

    x = np.ones((ydim, 1), dtype=int) * ((np.arange(xdim) - xdim // 2) * dx_surf)
    y = (np.ones((xdim, 1), dtype=int) * ((np.arange(ydim) - ydim // 2) * dx_surf)).T

    a = xtilt * np.pi / 180
    b = ytilt * np.pi / 180
    g = ztilt * np.pi /180

    if XYZ:
        m = np.array([[cos(b)*cos(g), -cos(b)*sin(g), sin(b), 0],
            [cos(a)*sin(g) + sin(a)*sin(b)*cos(g), cos(a)*cos(g)-sin(a)*sin(b)*sin(g), -sin(a)*cos(b), 0],
            [sin(a)*sin(g)-cos(a)*sin(b)*cos(g), sin(a)*cos(g)+cos(a)*sin(b)*sin(g), cos(a)*cos(b), 0],
            [0, 0, 0, 1] ])
    else:
        m = np.array([	[cos(b)*cos(g), cos(g)*sin(a)*sin(b)-cos(a)*sin(g), cos(a)*cos(g)*sin(b)+sin(a)*sin(g), 0],
            [cos(b)*sin(g), cos(a)*cos(g)+sin(a)*sin(b)*sin(g), -cos(g)*sin(a)+cos(a)*sin(b)*sin(g), 0],
            [-sin(b), cos(b)*sin(a), cos(a)*cos(b), 0],
            [0, 0, 0, 1] ])

    # Forward project a square
    edge = np.array([[-1.0, -1.0, 0.0, 0.0],
                     [1.0, -1.0, 0.0, 0.0],
                     [1.0, 1.0, 0.0, 0.0],
                     [-1.0, 1.0, 0.0, 0.0]])
    new_xyz = np.dot(edge, m)

    # determine backward projection for screen-raster-to-DM-surce computation
    dx_dxs = (new_xyz[0, 0] - new_xyz[1, 0]) / (edge[0, 0] - edge[1, 0])
    dx_dys = (new_xyz[1, 0] - new_xyz[2, 0]) / (edge[1, 1] - edge[2, 1])
    dy_dxs = (new_xyz[0, 1] - new_xyz[1, 1]) / (edge[0, 0] - edge[1, 0])
    dy_dys = (new_xyz[1, 1] - new_xyz[2, 1]) / (edge[1, 1] - edge[2, 1])

    xs = (x/dx_dxs - y*dx_dys/(dx_dxs*dy_dys)) / \
        (1 - dy_dxs*dx_dys/(dx_dxs*dy_dys))
    ys = (y/dy_dys - x*dy_dxs/(dx_dxs*dy_dys)) / \
        (1 - dx_dys*dy_dxs/(dx_dxs*dy_dys))

    xdm = (xs + dm_xc * dx_dm) / dx_inf + xoff_grid
    ydm = (ys + dm_yc * dx_dm) / dx_inf + yoff_grid

    if proper.use_cubic_conv:
        grid = proper.prop_cubic_conv(dm_grid.T, xdm, ydm, GRID = False)
        grid = grid.reshape([xdm.shape[1], xdm.shape[0]])
    else:
        grid = map_coordinates(dm_grid.T, [xdm, ydm], order=3,
                               mode="nearest", prefilter = True)

    dmap = np.zeros([n, n], dtype=np.float64)
    nx_grid, ny_grid = grid.shape
    xmin, xmax = n // 2 - xdim // 2, n // 2 - xdim // 2 + nx_grid
    ymin, ymax = n // 2 - ydim // 2, n // 2 - ydim // 2 + ny_grid
    dmap[ymin:ymax, xmin:xmax] = grid

    if "NO_APPLY" not in kwargs:
        proper.prop_add_phase(wf, 2 * dmap)  # convert surface to WFE

    return dmap
Example #8
0
def adaptive_optics(wfo, iwf, iw, f_lens, beam_ratio, iter):
    # print 'Including Adaptive Optics'
    # PSF = proper.prop_shift_center(np.abs(wfo.wfarr)**2)

    # quicklook_im(obj_map, logAmp=False)

    # code to distort measured phase map goes here....
    # print 'add code to distort phase measurment'
    nact = tp.ao_act  #49                       # number of DM actuators along one axis
    nact_across_pupil = nact - 2  #/1.075#nact #47          # number of DM actuators across pupil
    dm_xc = (nact / 2) - 0.5  #-1#0.5#- 0.5
    dm_yc = (nact / 2) - 0.5  #-1#0.5#- 0.5
    d_beam = 2 * proper.prop_get_beamradius(wfo)  # beam diameter
    # dprint((d_beam, d_beam/nact_across_pupil))
    act_spacing = d_beam / (nact_across_pupil)  # actuator spacing
    map_spacing = proper.prop_get_sampling(wfo)  # map sampling

    # have passed through focus, so pupil has rotated 180 deg;
    # need to rotate error map (also need to shift due to the way
    # the rotate() function operates to recenter map)
    # obj_map = np.roll(np.roll(np.rot90(obj_map, 2), 1, 0), 1, 1)
    # obj_map = np.roll(obj_map, 1, -1)
    # quicklook_im(obj_map[45:83,45:83], logAmp=False, show=False)
    # plt.figure()
    # plt.plot(range(44, 84), obj_map[64, 44:84])

    # print map_spacing
    # interpolate map to match number of DM actuators
    # print map_spacing/act_spacing

    # print 'Interpolation boundary uncertainty is likely because of beam ratio causing a non-integer'
    # print '128*0.3 ~ 38 so thats what is being used for now. Needs some T.L.C.'

    # true_width = tp.grid_size*beam_ratio
    # width = int(np.ceil(true_width))
    # # width = int(np.ceil(tp.grid_size*beam_ratio))
    # if width%2 != 0:
    #     width += 1
    # exten = width/true_width
    # print exten
    # mid = int(tp.grid_size/2.)
    # lower = int(mid-width/2.)
    # upper = int(mid+width/2.)
    # print width, mid, lower, upper
    # #
    # # f= interpolate.interp2d(range(width+1), range(width+1), obj_map[lower:upper, lower:upper])
    # # dm_map = f(np.linspace(0,width+1,nact),np.linspace(0,width+1,nact))
    #
    # import skimage.transform
    # dm_map = skimage.transform.resize(obj_map[lower:upper, lower:upper], (nact,nact))
    # print map_spacing, act_spacing, map_spacing/act_spacing

    try:
        with open(iop.CPA_meas, 'rb') as handle:
            CPA_maps, iters = pickle.load(handle)
    except EOFError:
        print 'CPA file not ready?'
        import time
        time.sleep(10)
        with open(iop.CPA_meas, 'rb') as handle:
            CPA_maps, iters = pickle.load(handle)

    # loop_frames(CPA_maps, logAmp=False)
    if iwf[:9] == 'companion':
        CPA_map = CPA_maps[1, iw]
    else:
        CPA_map = CPA_maps[0, iw]

    # loop_frames(CPA_maps, logAmp=False)

    # quicklook_im(CPA_map, logAmp=False)

    # dprint((map_spacing, act_spacing, map_spacing/act_spacing))
    # dm_map = proper.prop_magnify(CPA_map, map_spacing/act_spacing, nact)

    dm_map = CPA_map[tp.grid_size / 2 -
                     (beam_ratio * tp.grid_size / 2):tp.grid_size / 2 +
                     (beam_ratio * tp.grid_size / 2) + 1, tp.grid_size / 2 -
                     (beam_ratio * tp.grid_size / 2):tp.grid_size / 2 +
                     (beam_ratio * tp.grid_size / 2) + 1]
    f = interpolate.interp2d(range(dm_map.shape[0]), range(dm_map.shape[0]),
                             dm_map)
    dm_map = f(np.linspace(0, dm_map.shape[0], nact),
               np.linspace(0, dm_map.shape[0], nact))
    # quicklook_im(dm_map, logAmp=False, show=True)
    dm_map = -dm_map * proper.prop_get_wavelength(wfo) / (4 * np.pi
                                                          )  #<--- here
    # dm_map = -dm_map * proper.prop_get_wavelength(wfo) / (2 * np.pi)
    # dm_map = np.zeros((65,65))
    # quicklook_im(dm_map, logAmp=False, show=True, colormap='jet')
    # if tp.piston_error:
    #     mean_dm_map = np.mean(np.abs(dm_map))
    #     var = mean_dm_map/200.#40.
    #     print var
    #     # var = 0.001#1e-11
    #     if var != 0.0:
    #         dm_map = dm_map + np.random.normal(0, var, (dm_map.shape[0], dm_map.shape[1]))
    # quicklook_im(dm_map, logAmp=False, show=True, colormap='jet')
    # plt.figure()
    # quicklook_wf(wfo)
    # plt.plot(np.linspace(44, 84, nact),dm_map[16])
    # plt.figure()
    # quicklook_im( obj_map[lower:upper, lower:upper], logAmp=False, show=True)
    # act_spacing /= 0.625
    # print act_spacing, proper.prop_get_beamradius(wfo)
    # Need to put on opposite pattern; convert wavefront error to surface height
    # plt.plot(range(44,84), proper.prop_get_phase(wfo)[64, 44:84])
    # proper.prop_add_phase(wfo, waffle)

    # quicklook_im(dm_map)
    if tp.active_null:
        with open(iop.NCPA_meas, 'rb') as handle:
            _, null_map, _ = pickle.load(handle)
        # dprint('null_map')
        # quicklook_im(null_map, logAmp=False)
        # dm_NCPA = -proper.prop_magnify(NCPA_map, map_spacing / act_spacing, nact)
        dm_NCPA = null_map * proper.prop_get_wavelength(wfo) / (4 * np.pi)

        # quicklook_im(dm_map, logAmp=False)
        dm_map += dm_NCPA
        # quicklook_im(dm_map, logAmp=False, show=True, colormap ='jet')
        # dm_map /= 2

    # if tp.speckle_kill:
    #     with open(iop.NCPA_meas, 'rb') as handle:
    #         Imaps, NCPA_map,_ = pickle.load(handle)
    # quicklook_im(NCPA_map, logAmp=False)
    # loop_frames(Imaps+1e-9, logAmp=True)
    # with open(iop.NCPA_meas, 'rb') as handle:
    #     _, NCPA_map,_ = pickle.load(handle)
    # quicklook_im(NCPA_map, logAmp=False)
    #
    # dm_NCPA = -proper.prop_magnify(NCPA_map, map_spacing / act_spacing, nact)
    # dm_NCPA = dm_NCPA*proper.prop_get_wavelength(wfo)/(4*np.pi)
    #
    # # quicklook_im(dm_map, logAmp=False)
    # dm_map = dm_NCPA

    if tp.active_modulate and iter >= 8:
        # import dm_functions as DM
        # # speck.generate_flatmap(phase)
        # s_amp = DM.amplitudemodel(0.05, 30, c=1.6)
        # tp.null_ao_act = tp.ao_act
        # xloc, yloc = 4, 0
        # # rotate_angle = iter%30 * 360/30
        # # rotate_angle = np.pi * rotate_angle / 180.
        # # rot_matrix = [[np.cos(rotate_angle), -np.sin(rotate_angle)], [np.sin(rotate_angle), np.cos(rotate_angle)]]
        # # xloc, yloc = np.matmul(rot_matrix, [xloc, yloc])
        #
        # phase = iter % 10 * 2 * np.pi / 10.
        # s_amp = iter % 5 * s_amp/ 5.
        # print xloc, yloc, phase
        # waffle = DM.make_speckle_kxy(xloc, yloc, s_amp, phase) / 1e6
        # waffle += DM.make_speckle_kxy(yloc, xloc, s_amp, -phase) / 1e6
        # waffle += DM.make_speckle_kxy(0.71 * xloc, 0.71 * xloc, s_amp, -phase) / 1e6
        # waffle += DM.make_speckle_kxy(0.71 * xloc, -0.71 * xloc, s_amp, -phase) / 1e6
        # waffle /= 4
        # # quicklook_im(waffle, logAmp=False)
        # # print dm_map.shape
        # # print waffle.shape
        #
        # dmap =proper.prop_dm(wfo, waffle, dm_xc, dm_yc, N_ACT_ACROSS_PUPIL=nact, FIT = True)
        # dm_map = -dm_map * proper.prop_get_wavelength(wfo) / (4 * np.pi * 5) * (iter % 10 - 5)
        print 1 / 5. * (iter % 10 - 5)
        dm_map = dm_map / 5. * (iter % 10 - 5)
        # dmap = proper.prop_dm(wfo, pattern, dm_xc, dm_yc,
        #                       N_ACT_ACROSS_PUPIL=nact, FIT=True)
    # quicklook_wf(wfo)

    # quicklook_wf(wfo)
    # dmap =proper.prop_dm(wfo, dm_map, dm_xc, dm_yc, N_ACT_ACROSS_PUPIL=nact, FIT = True) #<-- here

    dmap = proper.prop_dm(wfo, dm_map, dm_xc, dm_yc, act_spacing,
                          FIT=True)  #<-- here

    # quicklook_im(dmap, logAmp=False, show=True)
    # quicklook_im(CPA_map*proper.prop_get_wavelength(wfo)/(4*np.pi), logAmp=False)
    # quicklook_im(CPA_map*proper.prop_get_wavelength(wfo)/(4*np.pi) + dmap, logAmp=False, show=True)

    # # # There's a boundary effect that needs to be mitigated
    # phase_map = proper.prop_get_phase(wfo)
    # artefacts = np.abs(phase_map) > 1
    # # print np.shape(artefacts), np.shape(phase_map)
    # # quicklook_im(phase_map, logAmp=False)
    #
    # width = round(tp.grid_size*beam_ratio)
    # mid = int(tp.grid_size/2.)
    # lower = int(mid-width/2.)
    # upper = int(mid+width/2.)
    # mask = np.zeros((tp.grid_size, tp.grid_size))
    # mask[upper,:] = 1
    # mask[:, upper] = 1
    # mask[lower,:] = 1
    # mask[:,lower] = 1
    # mask = mask*artefacts
    #
    # # print width, upper
    #
    # # wfo.wfarr[upper] = phase_map[tp.grid_size/2,tp.grid_size/2]
    # # quicklook_im( phase_map*mask, logAmp=False)
    #
    # proper.prop_add_phase(wfo,-phase_map*mask*proper.prop_get_wavelength(wfo)/(2*np.pi))
    # quicklook_IQ(wfo)

    # I = np.real(wfo.wfarr)
    # Q = np.imag(wfo.wfarr)
    # I = proper.prop_shift_center(I)
    # Q = proper.prop_shift_center(Q)
    # Q[89] = 0
    # Q[:,89] = 0
    # # Q[39] = 0
    # # Q[:,39] = 0
    # I = proper.prop_shift_center(I)
    # Q = proper.prop_shift_center(Q)
    # wfo.wfarr = I+1j*Q
    # quicklook_wf(wfo)

    # quicklook_IQ(wfo)
    # print phase_map[artefacts]
    # plt.imshow(phase_map)
    # # plt.figure()
    # # plt.imshow(dmap)
    # #
    # plt.show()
    # #
    # proper.prop_propagate(wfo, f_lens, "coronagraph lens")
    # quicklook_wf(wfo)
    return
Example #9
0
def propcustom_zernikes(a, zernike_num, zernike_val, eps=0., **kwargs):
    """
    Add Zernike-polynomial wavefront errors to current wavefront.
    
    Noll ordering is used and a circular system is assumed. An arbitrary number
    of Zernikes normalized for an unobscured circular region can be computed,
    but only the first 22 Zernikes can be computed normalized for a
    centrally-obscured region.
    
    
    Parameters
    ----------
    a : object
        WaveFront class object
        
    zernike_num : numpy ndarray
        Scalar or 1D array specifying which Zernike polynomials to include
        
    zernike_val : numpy ndarray
        Scalar or 1D array containing Zernike coefficients (in meters of RMS
        wavefront phase error or dimensionless RMS amplitude error) for Zernike
        polynomials indexed by "zernike_num".
        
    eps : float
        Central obscuration ratio (0.0-1.0); default is 0.0
        
    
    Returns
    -------
        None
        Adds wavefront errors to current wavefront array
        
    dmap : numpy ndarray
        Aberration map
        
    
    Other Parameters
    ----------------
    AMPLITUDE : bool
        Optional keyword that specifies that the Zernike values in "zernike_val"
        represent the wavefront RMS amplitude (rather than phase) variation.
        The current wavefront will be multipled by the generated map.
        
    NAME : str
        String containing name of surface that will be printed when executed.
        
    NO_APPLY : bool
        If set, the aberration map will be generated but will not be applied to
        the wavefront. This is useful if you just want to generate a map for
        your own use and modification (e.g. to create an error map for a multi-
        segmented system, each with its own aberration map).
        
    RADIUS : float
        Optional keyword specifying the radius to which the Zernike polynomials
        are normalized. If not specified, the pilot beam radius is used.
    
    CENTERING : str
        String containing the centering of the array. "interpixel" centers the
        array between pixels. Any other value gives pixel centering.
    
    
    Raises
    ------
    ValueError:
        Maximum index for an obscured Zernike polynomial is 22
    
    
    Notes
    -----
    The user specifies 1D arrays containing the Zernike polynomial coefficient
    indicies, the respective coefficients, and if an obstructed circular aperture
    the central obscuration ratio. A wavefront error map will be generated and
    added to the current wavefront.
    
    Zernike index and corresponding aberration for 1st 22 zernikes
    1 : Piston
    2 : X tilt
    3 : Y tilt
    4 : Focus
    5 : 45 degree astigmatism
    6 : 0 degree astigmatism
    7 : Y coma
    8 : X coma
    9 : Y clover (trefoil)
    10 : X clover (trefoil)
    11 : 3rd order spherical
    12 : 5th order 0 degree astig
    13 : 5th order 45 degree astig
    14 : X quadrafoil
    15 : Y quadrafoil
    16 : 5th order X coma
    17 : 5th order Y coma
    18 : 5th order X clover
    19 : 5th order Y clover
    20 : X pentafoil
    21 : Y pentafoil
    22 : 5th order spherical
    
    Update - JEK - Fixed use of ** instead of ** in obscured Zernikes
    Update - AR - Added CENTERING as keyword
    """
    zernike_num = np.asarray(zernike_num)
    zernike_val = np.asarray(zernike_val)
    n = proper.n
    
    if proper.print_it and not ("NO_APPLY" in kwargs and kwargs["NO_APPLY"]):
        if "NAME" in kwargs:
            print("Applying aberrations at %s" % kwargs["NAME"])
        else:
            print("Applying aberrations")
            
    max_z = zernike_num.max()
    
    if eps != 0. and max_z > 22:
        raise ValueError("PROP_ZERNIKES: Maximum index for an obscured Zernike polynomial is 22.")
        
    dmap = np.zeros([n, n], dtype=np.float64)
    
    if "RADIUS" not in kwargs:
        beam_radius = proper.prop_get_beamradius(a)
    else:
        beam_radius = kwargs["RADIUS"]
        
    dx = proper.prop_get_sampling(a) / beam_radius
    x_offset = 0.
    y_offset = 0.
    if("CENTERING" in kwargs):
        if kwargs["CENTERING"] not in _VALID_CENTERING:
            raise ValueError(_CENTERING_ERR)
    
        # Shift by half pixel
        if('interpixel' in kwargs["CENTERING"]):
            x_offset = dx/2.
            y_offset = dx/2.

    x = (np.arange(n, dtype=np.float64) - n//2) * dx + x_offset
    # x_pow_2 = x**2
    
    if (eps == 0.):
        # get list of executable equations defining Zernike polynomials
        zlist, maxrp, maxtc = proper.prop_noll_zernikes(max_z, COMPACT=True,
                                                        EXTRA_VALUES=True)
        
        for j in range(n):
            ab = np.zeros(n, dtype=np.float64)
            y = (j - n//2) * dx + y_offset
            r = np.sqrt(x**2 + y**2)
            t = np.arctan2(y, x)
        
            # predefine r**power, cos(const*theta), sin(const*theta) vectors
            for i in range(2, maxrp+1):
                rps = str(i).strip()
                cmd = "r_pow_" + rps + " = r**i"
                exec(cmd)
            
            for i in range(1, maxtc+1):
                tcs = str(i).strip()
                cmd = "cos" + tcs + "t = np.cos(i*t)"
                exec(cmd)
                cmd = "sin" + tcs + "t = np.sin(i*t)"
                exec(cmd)
           
            # assemble aberrations
            for iz in range(zernike_num.size):
                tmp = eval(zlist[zernike_num[iz]])
                ab += zernike_val[iz] * tmp
            
            dmap[j, :] += ab
            
    else:
        
        for j in range(n):
            y = (j-n//2) * dx + y_offset
            r = np.sqrt(x**2 + y**2)
            r2 = r**2
            r3 = r**3
            r4 = r**4
            r5 = r**5
            t = np.arctan2(y, x)
            
            for iz in range(len(zernike_num)):
                if zernike_num[iz] == 1:
                    ab = 1.
                elif zernike_num[iz] == 2:
                    ab = (2*r*np.cos(t))/np.sqrt(1 + eps**2)
                elif zernike_num[iz] == 3:
                    ab = (2*r*np.sin(t))/np.sqrt(1 + eps**2)
                elif zernike_num[iz] == 4:
                    ab = (np.sqrt(3)*(1 + eps**2 - 2*r2))/(-1 + eps**2)
                elif zernike_num[iz] == 5:
                    ab = (np.sqrt(6)*r2*np.sin(2*t))/np.sqrt(1 + eps**2 + eps**4)
                elif zernike_num[iz] == 6:
                    ab = (np.sqrt(6)*r2*np.cos(2*t))/np.sqrt(1 + eps**2 + eps**4)
                elif zernike_num[iz] == 7:
                    ab = (2*np.sqrt(2)*r*(2 + 2*eps**4 - 3*r2 + eps**2*(2 - 3*r2))*np.sin(t))/((-1 + eps**2)*np.sqrt(1 + 5*eps**2 + 5*eps**4 + eps**6))
                elif zernike_num[iz] == 8:
                    ab = (2*np.sqrt(2)*r*(2 + 2*eps**4 - 3*r2 + eps**2*(2 - 3*r2))*np.cos(t))/((-1 + eps**2)*np.sqrt(1 + 5*eps**2 + 5*eps**4 + eps**6))
                elif zernike_num[iz] == 9:
                    ab = (2*np.sqrt(2)*r3*np.sin(3*t))/np.sqrt(1 + eps**2 + eps**4 + eps**6)
                elif zernike_num[iz] == 10:
                    ab = (2*np.sqrt(2)*r3*np.cos(3*t))/np.sqrt(1 + eps**2 + eps**4 + eps**6)
                elif zernike_num[iz] == 11:
                    ab = (np.sqrt(5)*(1 + eps**4 - 6*r2 + 6*r4 + eps**2*(4 - 6*r2)))/ (-1 + eps**2)**2
                elif zernike_num[iz] == 12:
                    ab = (np.sqrt(10)*r2*(3 + 3*eps**6 - 4*r2 + eps**2*(3 - 4*r2) + eps**4*(3 - 4*r2))*np.cos(2*t))/ ((-1 + eps**2)*np.sqrt((1 + eps**2 + eps**4)*(1 + 4*eps**2 + 10*eps**4 + 4*eps**6 + eps**8)))
                elif zernike_num[iz] == 13:
                    ab = (np.sqrt(10)*r2*(3 + 3*eps**6 - 4*r2 + eps**2*(3 - 4*r2) + eps**4*(3 - 4*r2))*np.sin(2*t))/ ((-1 + eps**2)*np.sqrt((1 + eps**2 + eps**4)*(1 + 4*eps**2 + 10*eps**4 + 4*eps**6 + eps**8)))
                elif zernike_num[iz] == 14:
                    ab = (np.sqrt(10)*r4*np.cos(4*t))/np.sqrt(1 + eps**2 + eps**4 + eps**6 + eps**8)
                elif zernike_num[iz] == 15:
                    ab = (np.sqrt(10)*r4*np.sin(4*t))/np.sqrt(1 + eps**2 + eps**4 + eps**6 + eps**8)
                elif zernike_num[iz] == 16:
                    ab = (2*np.sqrt(3)*r*(3 + 3*eps**8 - 12*r2 + 10*r4 - 12*eps**6*(-1 + r2) + 2*eps**4*(15 - 24*r2 + 5*r4) + 4*eps**2*(3 - 12*r2 + 10*r4))*np.cos(t))/((-1 + eps**2)**2*np.sqrt((1 + 4*eps**2 + eps**4)* (1 + 9*eps**2 + 9*eps**4 + eps**6)))
                elif zernike_num[iz] == 17:
                    ab = (2*np.sqrt(3)*r*(3 + 3*eps**8 - 12*r2 + 10*r4 - 12*eps**6*(-1 + r2) + 2*eps**4*(15 - 24*r2 + 5*r4) + 4*eps**2*(3 - 12*r2 + 10*r4))*np.sin(t))/((-1 + eps**2)**2*np.sqrt((1 + 4*eps**2 + eps**4)* (1 + 9*eps**2 + 9*eps**4 + eps**6)))
                elif zernike_num[iz] == 18:
                    ab = (2*np.sqrt(3)*r3*(4 + 4*eps**8 - 5*r2 + eps**2*(4 - 5*r2) + eps**4*(4 - 5*r2) + eps**6*(4 - 5*r2))*np.cos(3*t))/ ((-1 + eps**2)*np.sqrt((1 + eps**2 + eps**4 + eps**6)*(1 + 4*eps**2 + 10*eps**4 + 20*eps**6 + 10*eps**8 + 4*eps**10 + eps**12)))
                elif zernike_num[iz] == 19:
                    ab = (2*np.sqrt(3)*r3*(4 + 4*eps**8 - 5*r2 + eps**2*(4 - 5*r2) + eps**4*(4 - 5*r2) + eps**6*(4 - 5*r2))*np.sin(3*t))/ ((-1 + eps**2)*np.sqrt((1 + eps**2 + eps**4 + eps**6)*(1 + 4*eps**2 + 10*eps**4 + 20*eps**6 + 10*eps**8 + 4*eps**10 + eps**12)))
                elif zernike_num[iz] == 20:
                    ab = (2*np.sqrt(3)*r5*np.cos(5*t))/ np.sqrt(1 + eps**2 + eps**4 + eps**6 + eps**8 + eps**10)
                elif zernike_num[iz] == 21:
                    ab = (2*np.sqrt(3)*r5*np.sin(5*t))/ np.sqrt(1 + eps**2 + eps**4 + eps**6 + eps**8 + eps**10)
                elif zernike_num[iz] == 22:
                    ab = (np.sqrt(7)*(1 + eps**6 - 12*r2 + 30*r4 - 20*r**6 + eps**4*(9 - 12*r2) + eps**2*(9 - 36*r2 + 30*r4)))/ (-1 + eps**2)**3
                    
            dmap[j, :] += zernike_val[iz] * ab
            
    if not ("NO_APPLY" in kwargs and kwargs["NO_APPLY"]):
        if ("AMPLITUDE" in kwargs and kwargs["AMPLITUDE"]):
            a.wfarr *= proper.prop_shift_center(dmap)
        else:
            i = complex(0, 1)
            a.wfarr *= np.exp(i*2*np.pi/a.lamda*proper.prop_shift_center(dmap))
            
    return dmap