Beispiel #1
0
    def test_real_positive_scalar_good(self):
        """
        Verify checker works correctly for valid input.

        Type: real positive scalar
        """
        try:
            check.real_positive_scalar(1, 'rps', TestCheckException)
        except check.CheckException:
            self.fail('real_positive_scalar failed on valid input')
        pass
Beispiel #2
0
    def test_real_positive_scalar_bad_var(self):
        """
        Fail on invalid variable type.

        Type: real positive scalar
        """
        for v0 in [-1, 1j, (1., ), [5, 5], 'v0']:
            with self.assertRaises(TestCheckException):
                check.real_positive_scalar(v0, 'rps', TestCheckException)
                pass
            pass
        pass
Beispiel #3
0
def mft_f2p(E_foc, fl, wavelength, dxi, deta, dx, N, centering='pixel'):
    """
    Propagate a field from a focal plane to a pupil plane, using a matrix-multiply DFT.

    Parameters
    ----------
    E_foc : array_like
        Electric field array in focal plane
    fl : float
        Focal length of Fourier transforming lens
    wavelength : float
        Propagation wavelength
    dxi : float
        Step size along horizontal axis of focal plane
    deta : float
        Step size along vertical axis of focal plane
    dx : float
        Step size along either axis of focal plane.  The vertical and horizontal step sizes are
        assumed to be equal.
    N : int
        Number of datapoints along each side of the pupil-plane (output) array
    centering : string
        Whether the input and output arrays are pixel-centered or inter-pixel-centered.
        Possible values: 'pixel', 'interpixel'

    Returns
    -------
    array_like
        Field in pupil plane, after propagating through Fourier transforming lens

    """
    if centering not in _VALID_CENTERING:
        raise ValueError(_CENTERING_ERR)
    check.twoD_array(E_foc, 'E_foc', TypeError)
    check.real_scalar(fl, 'fl', TypeError)
    check.real_positive_scalar(wavelength, 'wavelength', TypeError)
    check.real_positive_scalar(dxi, 'dxi', TypeError)
    check.real_positive_scalar(deta, 'deta', TypeError)
    check.real_positive_scalar(dx, 'dx', TypeError)
    check.positive_scalar_integer(N, 'N', TypeError)

    Neta, Nxi = E_foc.shape
    dy = dx  # Assume equal sample spacing along both directions

    # Focal-plane coordinates
    xi = util.create_axis(Nxi, dxi, centering=centering)[:, None]  # Broadcast to column vector
    eta = util.create_axis(Neta, dxi, centering=centering)[None, :]  # Broadcast to row vector

    # Pupil-plane coordinates
    x = util.create_axis(N, dx, centering=centering)[None, :]  # Broadcast to row vector
    y = x.T  # Column vector

    # Fourier transform matrices
    pre = np.exp(-2 * np.pi * 1j * (y * eta) / (wavelength * fl))
    post = np.exp(-2 * np.pi * 1j * (xi * x) / (wavelength * fl))

    # Constant scaling factor in front of Fourier transform
    scaling = np.sqrt(dx * dy * dxi * deta) / (1 * wavelength * fl)

    return scaling * np.linalg.multi_dot([pre, E_foc, post])
Beispiel #4
0
def ptp(E_in, full_width, wavelength, dz):
    """
    Propagate an electric field array using the angular spectrum technique.

    Parameters
    ----------
    E_in : array_like
        Square (i.e. NxN) input array.
    full_width : float
        The width along each side of the array [meters]
    wavelength : float
        Propagation wavelength [meters]
    dz : float
        Axial propagation distance [meters]

    Returns
    -------
    array_like
        Field after propagating over distance dz.

    """
    check.twoD_array(E_in, 'E_in', TypeError)
    check.real_positive_scalar(full_width, 'full_width', TypeError)
    check.real_positive_scalar(wavelength, 'wavelength', TypeError)
    check.real_scalar(dz, 'dz', TypeError)
    
    M, N = E_in.shape
    dx = full_width / N
    N_critical = int(np.floor(wavelength * np.abs(dz) / (dx ** 2)))  # Critical sampling

    if M != N:  # Input array is not square
        raise ValueError('Input array is not square')

    elif N < N_critical:
        log.warning(
             '''
             Input array is undersampled.
                Minimum required samples:  {}
                                  Actual:  {}
             '''.format(N_critical, N))

    fx = np.arange(-N // 2, N // 2) / full_width
    rho = util.radial_grid(fx)  # Spatial frequency coordinate grid

    kernel = np.fft.fftshift(np.exp(-1j * np.pi * wavelength * dz * (rho ** 2)))
    intermediate = np.fft.fftn(np.fft.fftshift(E_in))

    return np.fft.ifftshift(np.fft.ifftn(kernel * intermediate))
Beispiel #5
0
 def test_real_positive_scalar_bad_vexc(self):
     """Fail on input vexc not an Exception."""
     with self.assertRaises(check.CheckException):
         check.real_positive_scalar(1, 'rps', 'TestCheckException')
         pass
     pass
Beispiel #6
0
 def test_real_positive_scalar_bad_vname(self):
     """Fail on invalid input name for user output."""
     with self.assertRaises(check.CheckException):
         check.real_positive_scalar(1, (1, ), TestCheckException)
         pass
     pass
Beispiel #7
0
def derotate_resize_surface(surfaceToFit, dx, Nact, dm_xc, dm_yc, spacing,
                            **kwargs):
    """
    Derotate and resize a DM surface to size and alignment of actuator grid.
    
    Does the order of operations in the reverse order of PROPER's prop_dm.


    Parameters
    ----------
    surfaceToFit : numpy ndarray
        2-D DM surface map to be fitted
    dx : float
        width of a pixel in meters
    Nact : int
        number of actuators across the DM array
    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
        Spacing in meters between actuator centers (aka the pitch).

    Returns
    -------
    gridDerotAtActRes : numpy ndarray
        Returns DM surface at same alignment and resolution as the DM actuator
        array.

    Other Parameters
    ----------------

    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 ZYX and XYZ rotations.

    """
    check.twoD_array(surfaceToFit, 'surfaceToFit', TypeError)
    check.real_positive_scalar(dx, 'dx', TypeError)
    check.real_scalar(dm_xc, 'dm_xc', TypeError)
    check.real_scalar(dm_yc, 'dm_yc', TypeError)
    check.real_positive_scalar(spacing, 'spacing', TypeError)
    
    if "ZYX" in kwargs and "XYZ" in kwargs:
        raise ValueError('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.

    dm_z = np.eye(Nact)
    
    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 = surfaceToFit.shape[0]
    dx_surf = dx  # sampling of current surface in meters

    # 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.e-3  # spacing between DM actuators in meters assumed by influence function
    inf_mag = 10

    dx_dm = spacing

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

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

    # Compute xdm0 and ydm0 for use in de-rotating the DM surface
    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 = edge #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))

    xdm0 = (xs + dm_xc * dx_dm) / dx_inf + xoff_grid
    ydm0 = (ys + dm_yc * dx_dm) / dx_inf + yoff_grid
    ######

    # 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)
    # dm_grid = falco.util.pad_crop(surfaceToFit, xdm.shape[0])

    # Derotate the DM surface
    dm_grid = falco.util.pad_crop(surfaceToFit, xdm.shape[0])
    gridDerot = griddata((xdm.flatten(), ydm.flatten()), dm_grid.flatten(),
                         (xdm0, ydm0), method='cubic', fill_value=0.)
    # gridDerot(isnan(gridDerot)) = 0

    # Resize and decimate the DM surface to get it at the same size as the DM
    # actuator command array.
    #  The result will be fed to fit_surf_to_act() for deconvolution with the
    #  influence function.
    xOffsetInAct = ((Nact/2 - 1/2) - dm_xc)
    yOffsetInAct = ((Nact/2 - 1/2) - dm_yc)
 
    multipleOfCommandGrid = int(falco.util.ceil_odd(spacing/dx))
    N1 = Nact*multipleOfCommandGrid
    N2 = dm_grid.shape[0]
    xs1 = np.linspace(-(N1-1)/2, (N1-1)/2, N1)/N1  # interpixel centered
    if(N2 % 2 == 0):
        xs2 = np.linspace(-N2/2, (N2/2)-1, N2)/N2*(N2*dx/(Nact*spacing))
    else:
        xs2 = np.linspace(-(N2-1)/2, (N2-1)/2, N2)/N2*(N2*dx/(Nact*spacing))

    interp_spline = RectBivariateSpline(xs2, xs2, gridDerot)
    gridDerotResize = interp_spline(xs1-xOffsetInAct/Nact,
                                    xs1-yOffsetInAct/Nact)
        
    xyOffset = int(np.floor(multipleOfCommandGrid/2.))
    gridDerotAtActRes = gridDerotResize[xyOffset::multipleOfCommandGrid,
                                        xyOffset::multipleOfCommandGrid]
#
#    plt.figure(11); plt.imshow(dm_grid); plt.colorbar(); plt.pause(0.1)
#    plt.figure(12); plt.imshow(gridDerot); plt.colorbar(); plt.pause(0.1)
#    plt.figure(13); plt.imshow(gridDerotResize); plt.colorbar(); plt.pause(0.1)
#    plt.figure(14); plt.imshow(gridDerotAtActRes); plt.colorbar(); plt.pause(0.1)
#    plt.figure(15); plt.imshow(gridDerot-gridDerot[::-1,::-1]); plt.colorbar(); plt.pause(0.1)
#    plt.figure(16); plt.imshow(gridDerotResize-gridDerotResize[::-1,::-1]); plt.colorbar(); plt.pause(0.1)

    return gridDerotAtActRes
Beispiel #8
0
def gen_poke_cube(dm, mp, dx_dm, **kwargs):
    """
    Compute the datacube of each influence function.

    Influence functions are cropped down or padded up
    to the best size for angular spectrum propagation.

    Parameters
    ----------
    dm : ModelParameters
        Structure containing parameter values for the DM
    mp: falco.config.ModelParameters
        Structure of model parameters
    dx_dm : float
        Pixel width [meters] at the DM plane

    Other Parameters
    ----------------
    NOCUBE : bool
       Switch that tells function not to compute the datacube of influence
       functions.

    Returns
    -------
    None
        modifies structure "dm" by reference

    """
    if type(mp) is not falco.config.ModelParameters:
        raise TypeError('Input "mp" must be of type ModelParameters')
    check.real_positive_scalar(dx_dm, 'dx_dm', TypeError)

    if "NOCUBE" in kwargs and kwargs["NOCUBE"]:
        flagGenCube = False
    else:
        flagGenCube = True

    # Define this flag if it doesn't exist in the older code for square actuator arrays only
    if not hasattr(dm, 'flag_hex_array'):
        dm.flag_hex_array = False

    # Set the order of operations
    XYZ = True
    if(hasattr(dm, 'flagZYX')):
        if(dm.flagZYX):
            XYZ = False

    # Compute sampling of the pupil. Assume that it is square.
    dm.dx_dm = dx_dm
    dm.dx = dx_dm

    # Default to being centered on a pixel if not specified
    if not(hasattr(dm, 'centering')):
        dm.centering = 'pixel'

    # Compute coordinates of original influence function
    Ninf0 = dm.inf0.shape[0]  # Number of points across inf func at native res
    x_inf0 = np.linspace(-(Ninf0-1)/2., (Ninf0-1)/2., Ninf0)*dm.dx_inf0
    # True for even- or odd-sized influence function maps as long as they are
    # centered on the array.
    [Xinf0, Yinf0] = np.meshgrid(x_inf0, x_inf0)

    # Number of points across the DM surface at native inf func resolution
    Ndm0 = falco.util.ceil_even(Ninf0 + (dm.Nact - 1)*(dm.dm_spacing/dm.dx_inf0))
    # Number of points across the (un-rotated) DM surface at new, desired res.
    dm.NdmMin = falco.util.ceil_even(Ndm0*(dm.dx_inf0/dm.dx))+2.
    # Number of points across the array to fully contain the DM surface at new
    # desired resolution and z-rotation angle.
    dm.Ndm = int(falco.util.ceil_even((abs(np.array([np.sqrt(2.)*cos(radians(45.-dm.zrot)),
            np.sqrt(2.)*sin(radians(45.-dm.zrot))])).max())*Ndm0*(dm.dx_inf0/dm.dx))+2)

    # Compute list of initial actuator center coordinates (in actutor widths).
    if(dm.flag_hex_array):  # Hexagonal, hex-packed grid
        raise ValueError('flag_hex_array option not implemented yet.')
#     Nrings = dm.Nrings;
#     x_vec = [];
#     y_vec = [];
#     % row number (rowNum) is 1 for the center row and 2 is above it, etc.
#     % Nacross is the total number of segments across that row
#     for rowNum = 1:Nrings
#         Nacross = 2*Nrings - rowNum; % Number of actuators across at that row (for hex tiling in a hex shape)
#         yval = sqrt(3)/2*(rowNum-1);
#         bx = Nrings - (rowNum+1)/2; % x offset from origin
# 
#         xs = (0:Nacross-1).' - bx; % x values are 1 apart
#         ys = yval*ones(Nacross,1); % same y-value for the entire row
# 
#         if(rowNum==1)
#             x_vec = [x_vec;xs];
#             y_vec = [y_vec;ys]; 
#         else
#             x_vec = [x_vec;xs;xs];
#             y_vec = [y_vec;ys;-ys]; % rows +/-n have +/- y coordinates
#         end
#     end
    else:  # Square grid [actuator widths]
        [dm.Xact, dm.Yact] = np.meshgrid(np.arange(dm.Nact) - 
                                         dm.xc, np.arange(dm.Nact)-dm.yc)
#        # Use order='F' to compare the final datacube to Matlab's output.
#        #  Otherwise, use C ordering for Python FALCO.
#        x_vec = dm.Xact.reshape(dm.Nact*dm.Nact,order='F')
#        y_vec = dm.Yact.reshape(dm.Nact*dm.Nact,order='F')
        x_vec = dm.Xact.reshape(dm.Nact*dm.Nact)
        y_vec = dm.Yact.reshape(dm.Nact*dm.Nact)

    dm.NactTotal = x_vec.shape[0]  # Total number of actuators in the 2-D array
    dm.xy_cent_act = np.zeros((2, dm.NactTotal))  # Initialize

    # Compute the rotation matrix to apply to the influence function and
    #  actuator center locations
    tlt = np.zeros(3)
    tlt[0] = radians(dm.xtilt)
    tlt[1] = radians(dm.ytilt)
    tlt[2] = radians(-dm.zrot)

    sa = sin(tlt[0])
    ca = cos(tlt[0])
    sb = sin(tlt[1])
    cb = cos(tlt[1])
    sg = sin(tlt[2])
    cg = cos(tlt[2])

    if XYZ:
        Mrot = np.array(
            [[cb * cg, sa * sb * cg - ca * sg, ca * sb * cg + sa * sg, 0.0],
             [cb * sg, sa * sb * sg + ca * cg, ca * sb * sg - sa * cg, 0.0],
             [-sb, sa * cb, ca * cb, 0.0],
             [0.0, 0.0, 0.0, 1.0]])
    else:
        Mrot = np.array(
            [[cb * cg, -cb * sg, sb, 0.0],
             [ca * sg + sa * sb * cg, ca * cg - sa * sb * sg, -sa * cb, 0.0],
             [sa * sg - ca * sb * cg, sa * cg + ca * sb * sg, ca * cb, 0.0],
             [0.0, 0.0, 0.0, 1.0]])

    # # Compute the actuator center coordinates in units of actuator spacings
    # for iact in range(dm.NactTotal):
    #     xyzVals = np.array([x_vec[iact], y_vec[iact], 0., 1.])
    #     xyzValsRot = Mrot @ xyzVals
    #     dm.xy_cent_act[0, iact] = xyzValsRot[0].copy()
    #     dm.xy_cent_act[1, iact] = xyzValsRot[1].copy()

    actIndMat = np.arange(dm.Nact**2, dtype=int).reshape((dm.Nact, dm.Nact))
    if hasattr(dm, 'orientation'):
        if dm.orientation.lower() == 'rot0':
            pass  # no change
        elif dm.orientation.lower() == 'rot90':
            actIndMat = np.rot90(actIndMat, 1)
        elif dm.orientation.lower() == 'rot180':
            actIndMat = np.rot90(actIndMat, 2)
        elif dm.orientation.lower() == 'rot270':
            actIndMat = np.rot90(actIndMat, 3)
        elif dm.orientation.lower() == 'flipxrot0':
            actIndMat = np.flipx(actIndMat)
        elif dm.orientation.lower() == 'flipxrot90':
            actIndMat = np.rot90(np.flipx(actIndMat), 1)
        elif dm.orientation.lower() == 'flipxrot180':
            actIndMat = np.rot90(np.flipx(actIndMat), 2)
        elif dm.orientation.lower() == 'flipxrot270':
            actIndMat = np.rot90(np.flipx(actIndMat), 3)
        else:
            raise ValueError('invalid value of dm.orientation')

    # Compute the actuator center coordinates in units of actuator spacings
    for iact, iIndex in enumerate(actIndMat.flatten()):
        xyzVals = np.array([x_vec[iIndex], y_vec[iIndex], 0., 1.])
        xyzValsRot = Mrot @ xyzVals
        dm.xy_cent_act[0, iact] = xyzValsRot[0].copy()
        dm.xy_cent_act[1, iact] = xyzValsRot[1].copy()

    N0 = dm.inf0.shape[0]
    Npad = falco.util.ceil_odd(np.sqrt(2.)*N0)
    inf0pad = np.zeros((Npad, Npad))
    inf0pad[int(np.ceil(Npad/2.)-np.floor(N0/2.)-1):int(np.ceil(Npad/2.)+np.floor(N0/2.)),
            int(np.ceil(Npad/2.)-np.floor(N0/2.)-1):int(np.ceil(Npad/2.)+np.floor(N0/2.))] = dm.inf0

    ydim = inf0pad.shape[0]
    xdim = inf0pad.shape[1]

    xd2 = np.fix(xdim / 2.) + 1
    yd2 = np.fix(ydim / 2.) + 1
    cx = np.arange(xdim) + 1. - xd2
    cy = np.arange(ydim) + 1. - yd2
    [Xs0, Ys0] = np.meshgrid(cx, cy)

    xsNewVec = np.zeros(xdim*xdim)
    ysNewVec = np.zeros(ydim*ydim)
    Xs0Vec = Xs0.reshape(xdim*xdim)
    Ys0Vec = Ys0.reshape(ydim*ydim)

    for ii in range(Xs0.size):
        xyzVals = np.array([Xs0Vec[ii], Ys0Vec[ii], 0., 1.])
        xyzValsRot = Mrot @ xyzVals
        xsNewVec[ii] = xyzValsRot[0]
        ysNewVec[ii] = xyzValsRot[1]

    # Calculate the interpolated DM grid at the new resolution
    # (set extrapolated values to 0.0)
    dm.infMaster = griddata((xsNewVec, ysNewVec), inf0pad.reshape(Npad*Npad),
                            (Xs0, Ys0), method='cubic', fill_value=0.)

    # Crop down the influence function until it has no zero padding left
    infSum = np.sum(dm.infMaster)
    infDiff = 0.
    counter = 0
    while(abs(infDiff) <= 1e-7):
        counter = counter + 2
        infDiff = infSum - np.sum(abs(dm.infMaster[int(counter/2):int(-counter/2),
                                                   int(counter/2):int(-counter/2)]))

    # Subtract an extra 2 to negate the extra step that overshoots.
    counter = counter - 2
    Ninf0pad = dm.infMaster.shape[0]-counter
    if counter == 0:
        infMaster2 = dm.infMaster.copy()
    else:
        # The cropped-down influence function
        infMaster2 = dm.infMaster[int(counter/2):int(-counter/2),
                                  int(counter/2):int(-counter/2)].copy()
        dm.infMaster = infMaster2

    Npad = Ninf0pad

    # True for even- or odd-sized influence function maps as long as they are
    # centered on the array.
    x_inf0 = np.linspace(-(Npad-1)/2, (Npad-1)/2., Npad)*dm.dx_inf0
    [Xinf0, Yinf0] = np.meshgrid(x_inf0, x_inf0)

    # Translate and resample the master influence function to be at each 
    # actuator's location in the pixel grid

    # Compute the size of the postage stamps.
    # Number of points across the influence function array at the DM plane's
    # resolution. Want as even
    Nbox = falco.util.ceil_even(Ninf0pad*dm.dx_inf0/dx_dm)
    dm.Nbox = Nbox
    # Also compute their padded sizes for the angular spectrum (AS) propagation
    # between P2 and DM1 or between DM1 and DM2
    # Minimum number of points across for accurate angular spectrum propagation
    Nmin = falco.util.ceil_even(np.max(mp.sbp_centers)*np.max(np.abs(np.array(
        [mp.d_P2_dm1, mp.d_dm1_dm2, (mp.d_P2_dm1+mp.d_dm1_dm2)])))/dx_dm**2)
    # Use a larger array if the max sampling criterion for angular spectrum
    # propagation is violated
    dm.NboxAS = np.max(np.array([Nbox, Nmin]))

    # Pad the pupil to at least the size of the DM(s) surface(s) to allow all
    # actuators to be located outside the pupil.
    # (Same for both DMs)

    # Find actuator farthest from center:
    dm.r_cent_act = np.sqrt(dm.xy_cent_act[0, :]**2 + dm.xy_cent_act[1, :]**2)
    dm.rmax = np.max(np.abs(dm.r_cent_act))
    NpixPerAct = dm.dm_spacing/dx_dm
    if(dm.flag_hex_array):
        # padded 2 actuators past the last actuator center to avoid trying to
        # index outside the array
        dm.NdmPad = falco.util.ceil_even((2.*(dm.rmax+2))*NpixPerAct + 1)
    else:
        # DM surface array padded by the width of the padded influence function
        # to prevent indexing outside the array. 
        # The 1/2 term is because the farthest actuator center is still half an
        # actuator away from the nominal array edge. 
        dm.NdmPad = falco.util.ceil_even((dm.NboxAS + 2.0*(1 + (np.max(
        np.abs(dm.xy_cent_act.reshape(2*dm.NactTotal)))+0.5)*NpixPerAct)))

    # Compute coordinates (in meters) of the full DM array
    if(dm.centering == 'pixel'):
        # meters, coords for the full DM arrays. Origin is centered on a pixel
        dm.x_pupPad = np.linspace(-dm.NdmPad/2., (dm.NdmPad/2. - 1),
                                  dm.NdmPad)*dx_dm
    else:
        # meters, coords for the full DM arrays. Origin is interpixel centered
        dm.x_pupPad = np.linspace(-(dm.NdmPad-1)/2., (dm.NdmPad-1)/2.,
                                  dm.NdmPad)*dx_dm

    dm.y_pupPad = dm.x_pupPad

    dm.act_ele = np.arange(dm.NactTotal)  # Include all actuators

    # Make NboxPad-sized postage stamps for each actuator's influence function
    if(flagGenCube):
        if not dm.flag_hex_array:
            print("  Influence function padded from %d to %d points for A.S. propagation." % (Nbox, dm.NboxAS))

        print('Computing datacube of DM influence functions... ', end='')

        # Find the locations of the postage stamps arrays in the larger pupilPad array
        dm.xy_cent_act_inPix = dm.xy_cent_act*(dm.dm_spacing/dx_dm)  # Convert units to pupil-plane pixels
        dm.xy_cent_act_inPix = dm.xy_cent_act_inPix + 0.5  # For the half-pixel offset if pixel centered. 
        dm.xy_cent_act_box = np.round(dm.xy_cent_act_inPix)  # Center locations of the postage stamps (in between pixels), in actuator widths
        dm.xy_cent_act_box_inM = dm.xy_cent_act_box*dx_dm  # now in meters
        dm.xy_box_lowerLeft = dm.xy_cent_act_box + (dm.NdmPad-Nbox)/2 - 0  # index of pixel in lower left of the postage stamp within the whole pupilPad array. +0 for Python, +1 for Matlab

        # Starting coordinates (in actuator widths) for updated master influence function.
        # This is interpixel centered, so do not translate!
        dm.x_box0 = np.linspace(-(Nbox-1)/2., (Nbox-1)/2., Nbox)*dx_dm
        [dm.Xbox0, dm.Ybox0] = np.meshgrid(dm.x_box0, dm.x_box0)  # meters, interpixel-centered coordinates for the master influence function

        # (Allow for later) Limit the actuators used to those within 1 actuator width of the pupil
        # r_cent_act_box_inM = np.sqrt(dm.xy_cent_act_box_inM[0, :]**2 + dm.xy_cent_act_box_inM[1, :]**2)
        # Compute and store all the influence functions:
        dm.inf_datacube = np.zeros((Nbox, Nbox, dm.NactTotal))  # initialize array of influence function "postage stamps"
        inf_datacube = np.zeros((dm.NactTotal, Nbox, Nbox))

        interp_spline = RectBivariateSpline(x_inf0, x_inf0, dm.infMaster)  # RectBivariateSpline is faster in 2-D than interp2d
        # Refer to https://scipython.com/book/chapter-8-scipy/examples/two-dimensional-interpolation-with-scipyinterpolaterectbivariatespline/

        for iact in range(dm.NactTotal):
            xbox = dm.x_box0 - (dm.xy_cent_act_inPix[0, iact]-dm.xy_cent_act_box[0, iact])*dx_dm # X = X0 -(x_true_center-x_box_center)
            ybox = dm.x_box0 - (dm.xy_cent_act_inPix[1, iact]-dm.xy_cent_act_box[1, iact])*dx_dm # Y = Y0 -(y_true_center-y_box_center)
            dm.inf_datacube[:, :, iact] = interp_spline(ybox, xbox)
            inf_datacube[iact, :, :] = interp_spline(ybox, xbox)

        print('done.')

    else:
        dm.act_ele = np.arange(dm.NactTotal)
Beispiel #9
0
def gen_surf_from_act(dm, dx, N):
    """
    Function to compute the surface shape of a deformable mirror. Uses PROPER.

    Parameters
    ----------
    dm : ModelParameters
        Structure containing parameter values for the DM
    dx : float
        Pixel width [meters] at the DM plane
    N : int
        Number of points across the array to return at the DM plane

    Returns
    -------
    DMsurf : array_like
        2-D surface map of the DM

    """
    check.real_positive_scalar(dx, 'dx', TypeError)
    check.positive_scalar_integer(N, 'N', TypeError)
    # if type(dm) is not falco.config.Object:
    #     raise TypeError('Input "dm" must be of type falco.config.Object')

    # Set the order of operations
    flagXYZ = True
    if(hasattr(dm, 'flagZYX')):
        if(dm.flagZYX):
            flagXYZ = False

    # Adjust the centering of the output DM surface. The shift needs to be in
    # units of actuators, not meters, for prop_dm.m.
    Darray = dm.NdmPad*dm.dx
    Narray = dm.NdmPad
    if dm.centering == 'interpixel':
        cshift = -Darray/2./Narray/dm.dm_spacing
    elif dm.centering == 'pixel':
        cshift = 0

    pupil_ratio = 1  # beam diameter fraction
    wl_dummy = 1e-6  # dummy value needed to initialize PROPER (meters)

    bm = proper.prop_begin(N*dx, wl_dummy, N, pupil_ratio)

    # Apply various constraints to DM commands
    dm = enforce_constraints(dm)

    # Quantization of DM actuation steps based on least significant bit of the
    # DAC (digital-analog converter). In height, so called HminStep
    # If HminStep (minimum step in H) is defined, then quantize the DM voltages
    if(hasattr(dm, 'HminStep')):
        if not(hasattr(dm, 'HminStepMethod')):
            dm.HminStepMethod = 'round'
        # Discretize/Quantize the DM voltages (creates dm.Vquantized)
        dm = discretize_surf(dm, dm.HminStepMethod)
        heightMap = dm.VtoH*dm.Vquantized
    else:  # Quantization not desired; send raw, continuous voltages
        heightMap = dm.VtoH*dm.V

    if hasattr(dm, 'orientation'):
        if dm.orientation.lower() == 'rot0':
            pass  # no change
        elif dm.orientation.lower() == 'rot90':
            heightMap = np.rot90(heightMap, 1)
        elif dm.orientation.lower() == 'rot180':
            heightMap = np.rot90(heightMap, 2)
        elif dm.orientation.lower() == 'rot270':
            heightMap = np.rot90(heightMap, 3)
        elif dm.orientation.lower() == 'flipxrot0':
            heightMap = np.flipx(heightMap)
        elif dm.orientation.lower() == 'flipxrot90':
            heightMap = np.rot90(np.flipx(heightMap), 1)
        elif dm.orientation.lower() == 'flipxrot180':
            heightMap = np.rot90(np.flipx(heightMap), 2)
        elif dm.orientation.lower() == 'flipxrot270':
            heightMap = np.rot90(np.flipx(heightMap), 3)
        else:
            raise ValueError('invalid value of dm.orientation')

    # Generate the DM surface
    DMsurf = falco.dm.propcustom_dm(bm, heightMap, dm.xc-cshift, dm.yc-cshift,
    dm.dm_spacing, XTILT=dm.xtilt, YTILT=dm.ytilt, ZTILT=dm.zrot, XYZ=flagXYZ,
    inf_sign=dm.inf_sign, inf_fn=dm.inf_fn)

    return DMsurf
Beispiel #10
0
def mft_p2v2p(pupilPre, charge, beamRadius, inVal, outVal):
    """
    Propagate from the pupil plane before a vortex FPM to pupil plane after it.
    
    Compute a radial Tukey window for propagating through a vortex coroangraph.

    Parameters
    ----------
    pupilPre : array_like
        2-D E-field at pupil plane before the vortex focal plane mask
    charge : int, float
        Charge of the vortex mask
    beamRadius : float
        Beam radius at pupil plane. Units of pixels.
    inVal : float
        Ask Gary
    outVal : float
        Ask Gary
        
    Returns
    -------
    pupilPost : array_like
        2-D E-field at pupil plane after the vortex focal plane mask

    """
    check.twoD_array(pupilPre, 'pupilPre', TypeError)
    check.scalar_integer(charge, 'charge', TypeError)
    check.real_positive_scalar(beamRadius, 'beamRadius', TypeError)
    check.real_positive_scalar(inVal, 'inVal', TypeError)
    check.real_positive_scalar(outVal, 'outVal', TypeError)
    
    # showPlots2debug = False 

    D = 2.0*beamRadius
    lambdaOverD = 4. # samples per lambda/D
    
    NA = pupilPre.shape[1]
    NB = util.ceil_even(lambdaOverD*D)
    
    # [X,Y] = np.meshgrid(np.arange(-NB/2., NB/2., dtype=float),np.arange(-NB/2., NB/2., dtype=float))
    # [RHO,THETA] = util.cart2pol(Y,X)
    RHO = util.radial_grid(np.arange(-NB/2., NB/2., dtype=float))
   
    windowKnee = 1.-inVal/outVal
    
    windowMask1 = gen_tukey_for_vortex(2*outVal*lambdaOverD, RHO, windowKnee)
    windowMask2 = gen_tukey_for_vortex(NB, RHO, windowKnee)

    # DFT vectors 
    x = np.arange(-NA/2,NA/2,dtype=float)/D   #(-NA/2:NA/2-1)/D
    u1 = np.arange(-NB/2,NB/2,dtype=float)/lambdaOverD #(-NB/2:NB/2-1)/lambdaOverD
    u2 = np.arange(-NB/2,NB/2,dtype=float)*2*outVal/NB # (-NB/2:NB/2-1)*2*outVal/N
    
    FPM = falco_gen_vortex_mask(charge, NB)

    #if showPlots2debug; figure;imagesc(abs(pupilPre));axis image;colorbar; title('pupil'); end;

    ## Low-sampled DFT of entire region

    FP1 = 1/(1*D*lambdaOverD)*np.exp(-1j*2*np.pi*np.outer(u1,x)) @ pupilPre @ np.exp(-1j*2*np.pi*np.outer(x,u1))
    #if showPlots2debug; figure;imagesc(log10(abs(FP1).^2));axis image;colorbar; title('Large scale DFT'); end;

    LP1 = 1/(1*D*lambdaOverD)*np.exp(-1j*2*np.pi*np.outer(x,u1)) @ (FP1*FPM*(1-windowMask1)) @ np.exp(-1j*2*np.pi*np.outer(u1,x))
    #if showPlots2debug; figure;imagesc(abs(FP1.*(1-windowMask1)));axis image;colorbar; title('Large scale DFT (windowed)'); end;
    
    ## Fine sampled DFT of innter region
    FP2 = 2*outVal/(1*D*NB)*np.exp(-1j*2*np.pi*np.outer(u2,x)) @ pupilPre @ np.exp(-1j*2*np.pi*np.outer(x,u2))
    #if showPlots2debug; figure;imagesc(log10(abs(FP2).^2));axis image;colorbar; title('Fine sampled DFT'); end;
    FPM = falco_gen_vortex_mask(charge, NB)
    LP2 = 2.0*outVal/(1*D*NB)*np.exp(-1j*2*np.pi*np.outer(x,u2)) @ (FP2*FPM*windowMask2) @ np.exp(-1j*2*np.pi*np.outer(u2,x))       
    #if showPlots2debug; figure;imagesc(abs(FP2.*windowMask2));axis image;colorbar; title('Fine sampled DFT (windowed)'); end;
    pupilPost = LP1 + LP2;
    #if showPlots2debug; figure;imagesc(abs(pupilPost));axis image;colorbar; title('Lyot plane'); end;

    return pupilPost
Beispiel #11
0
def mft_p2f(E_pup, fl, wavelength, dx, dxi, Nxi, deta, Neta, centering='pixel'):
    """
    Propagate a pupil to a focus using a matrix-multiply DFT.

    Parameters
    ----------
    E_pup : array_like
        Electric field array in pupil plane
    fl : float
        Focal length of Fourier transforming lens
    wavelength : float
        Propagation wavelength
    dx : float
        Step size along either axis of focal plane.  The vertical and horizontal step sizes are
        assumed to be equal.
    dxi : float
        Step size along horizontal axis of focal plane
    Nxi : int
        Number of samples along horizontal axis of focal plane.
    deta : float
        Step size along vertical axis of focal plane
    Neta : int
        Number of samples along vertical axis of focal plane.
    centering : string
        Whether the input and output arrays are pixel-centered or inter-pixel-centered.
        Possible values: 'pixel', 'interpixel'

    Returns
    -------
    array_like
        Field in pupil plane, after propagating through Fourier transforming lens

    """
    if centering not in _VALID_CENTERING:
        raise ValueError(_CENTERING_ERR)
    check.twoD_array(E_pup, 'E_pup', TypeError)
    check.real_scalar(fl, 'fl', TypeError)
    check.real_positive_scalar(wavelength, 'wavelength', TypeError)
    check.real_positive_scalar(dx, 'dx', TypeError)
    check.real_positive_scalar(dxi, 'dxi', TypeError)
    check.positive_scalar_integer(Nxi, 'Nxi', TypeError)
    check.real_positive_scalar(deta, 'deta', TypeError)
    check.positive_scalar_integer(Neta, 'Neta', TypeError)

    dy = dx
    M, N = E_pup.shape
    if M != N:
        raise ValueError('Input array is not square')

    # Pupil-plane coordinates
    x = util.create_axis(N, dx, centering=centering)[:, None]  # Broadcast to column vector
    y = x.T  # Row vector

    # Focal-plane coordinates
    xi = util.create_axis(Nxi, dxi, centering=centering)[None, :]  # Broadcast to row vector
    eta = util.create_axis(Neta, deta, centering=centering)[:, None]  # Broadcast to column vector

    # Fourier transform matrices
    pre = np.exp(-2 * np.pi * 1j * (eta * y) / (wavelength * fl))
    post = np.exp(-2 * np.pi * 1j * (x * xi) / (wavelength * fl))

    # Constant scaling factor in front of Fourier transform
    scaling = np.sqrt(dx * dy * dxi * deta) / (1 * wavelength * fl)

    return scaling * np.linalg.multi_dot([pre, E_pup, post])
Beispiel #12
0
def solver(n, d0, theta, lam, tetm=False):
    """
    Solve the thin film equations for the given materials.

    Parameters
    ----------
    n : array_like
        index of refraction for each layer.
        n(1) = index of incident medium
        n(N) = index of transmission medium
        then length(n) must be >= 2
    d0 : array_like
        thickness of each layer, not counting incident medium or transmission
        medium. length(d) = length(n)-2.
    theta : float
        angle of incidence [radians].
    lam : float
        wavelength. units of lam must be same as d0.
    tetm : bool, optional
        False => TE, True => TM. The default is False.

    Returns
    -------
    R : numpy ndarray
        normalized reflected intensity coefficient
    T : numpy ndarray
        normalized transmitted intensity coefficient
    rr : numpy ndarray
        complex field reflection coefficient
    tt : numpy ndarray
        complex field transmission coefficient

    """
    oneD_array(n, 'n', ValueError)
    oneD_array(d0, 'd0', ValueError)
    N = len(n)
    if not (len(d0) == N-2):
        raise ValueError('n and d size mismatch')
        pass
    real_nonnegative_scalar(theta, 'theta', TypeError)
    real_positive_scalar(lam, 'lam', TypeError)
    if not type(tetm) == bool:
        raise TypeError('tetm must be a boolean.')
    
    # np.hstac
    d = np.hstack((0, d0.reshape(len(d0, )), 0))
        
    kx = 2*np.pi*n[0]*np.sin(theta)/lam
    # sign agrees with measurement convention:
    kz = -np.sqrt((2*np.pi*n/lam)**2 - kx*kx)
    
    if tetm:
        kzz = kz/(n*n)
    else:
        kzz = kz
    
    eep = np.exp(-1j*kz*d)
    eem = np.exp(1j*kz*d)
    
    i1 = np.arange(N-1)
    i2 = np.arange(1, N)
    tin = 0.5*(kzz[i1] + kzz[i2])/kzz[i1]
    ri = (kzz[i1] - kzz[i2])/(kzz[i1] + kzz[i2])
    
    A = np.eye(2, dtype=complex)
    for i in range(N-1):
        A = A @ np.array(tin[i]*np.array([[eep[i], ri[i]*eep[i]],
                                          [ri[i]*eem[i], eem[i]]]))
    
    rr = A[1, 0] / A[0, 0]
    tt = 1 / A[0, 0]
    
    # transmitted power flux (Poynting vector . surface) depends on index of
    # the substrate and angle
    R = np.abs(rr)**2
    if tetm:
        Pn = np.real((kz[-1]/(n[-1]**2)) / (kz[0]/(n[0]**2)))
    else:
        Pn = np.real((kz[-1]/kz[0]))
        pass
    
    T = Pn*np.abs(tt)**2
    tt = np.sqrt(Pn)*tt
    
    return [R, T, rr, tt]
Beispiel #13
0
def calc_complex_occulter(lam, aoi, t_Ti, t_Ni_vec, t_PMGI_vec,
                                 d0, pol, flagOPD=False, SUBSTRATE='FS'):
    """
    Calculate the thin-film complex transmission and reflectance.
    
    Calculates the thin-film complex transmission and reflectance for the
    provided combinations of metal and dielectric thicknesses and list of
    wavelengths.

    Parameters
    ----------
    lam : float
        Wavelength in meters.
    aoi : flaot
        Angle of incidence in degrees.
    t_Ti : float
        Titanium thickness in meters. Titanium goes only between
        fused silica and nickel layers.
    t_Ni_vec : array_like
        1-D array of nickel thicknesses in meters. Nickel goes between
        titanium and PMGI layers.
    t_PMGI_vec : array_like
        1-D array of PMGI thicknesses in meters.
    d0 : float
        Reference height for all phase offsets. Must be larger than the stack
        of materials, not including the substrate. Units of meters.
    pol : {0, 1, 2}
        Polarization state to compute values for.
        0 for TE(s) polarization,
        1 for TM(p) polarization,
        2 for mean of s and p polarizations
    flagOPD : bool, optional
        Flag to use the OPD convention. The default is False.
    SUBSTRATE : str, optional
        Material to use as the substrate. The default is 'FS'.

    Returns
    -------
    tCoef : numpy ndarray
        2-D array of complex transmission amplitude values.
    rCoef : numpy ndarray
        2-D array of complex reflection amplitude values.
    """
    real_positive_scalar(lam, 'lam', TypeError)
    real_nonnegative_scalar(aoi, 'theta', TypeError)
    real_nonnegative_scalar(t_Ti, 't_Ti', TypeError)
    oneD_array(t_Ni_vec, 't_Ni_vec', ValueError)
    oneD_array(t_PMGI_vec, 't_PMGI_vec', ValueError)
    # if len(t_Ti) != len(t_Ni_vec) or len(t_Ni_vec) != len(t_PMGI_vec):
    #     raise ValueError('Ti, Ni, and PMGI thickness vectors must all ' +
    #                      'have same length.')
    scalar_integer(pol, 'pol', TypeError)
    
    lam_nm = lam * 1.0e9  # m --> nm
    lam_um = lam * 1.0e6  # m --> microns
    lam_um2 = lam_um * lam_um
    theta = aoi * (np.pi/180.)  # deg --> rad
    
    # Define Material Properties
    # ---------------------------------------------
    # Substrate properties
    if SUBSTRATE.upper() in ('FS', 'FUSEDSILICA'):
        A1 = 0.68374049400
        A2 = 0.42032361300
        A3 = 0.58502748000
        B1 = 0.00460352869
        B2 = 0.01339688560
        B3 = 64.49327320000
        n_substrate = np.sqrt(1 + A1*lam_um2/(lam_um2 - B1) +
                           A2*lam_um2/(lam_um2 - B2) +
                           A3*lam_um2/(lam_um2 - B3))

    elif SUBSTRATE.upper() in ('N-BK7', 'NBK7', 'BK7'):
        B1 = 1.03961212
        B2 = 0.231792344
        B3 = 1.01046945
        C1 = 0.00600069867
        C2 = 0.0200179144
        C3 = 103.560653
        n_substrate = np.sqrt(1 + (B1*lam_um2/(lam_um2 - C1)) +
                              (B2*lam_um2/(lam_um2 - C2)) +
                              (B3*lam_um2/(lam_um2 - C3)))
    
    # Dielectric properties
    npmgi = 1.524 + 5.176e-03/lam_um**2 + 2.105e-4/lam_um**4
    Ndiel = len(t_PMGI_vec)
    
    # Metal layer properties
    # Titanium base layer under the nickel
    Nmetal = len(t_Ni_vec)
    t_Ti_vec = t_Ti * np.ones(Nmetal)
    t_Ti_vec[np.asarray(t_Ni_vec) < 1e-10] = 0  # no Ti where no Ni
    # from D Moody
    titanium = np.array([
                        [397, 2.08, 2.95],
                        [413, 2.14, 2.98],
                        [431, 2.21, 3.01],
                        [451, 2.27, 3.04],
                        [471, 2.3, 3.1],
                        [496, 2.36, 3.19],
                        [521, 2.44, 3.2],
                        [549, 2.54, 3.43],
                        [582, 2.6, 3.58],
                        [617, 2.67, 3.74],
                        [659, 2.76, 3.84],
                        [704, 2.86, 3.96],
                        [756, 3.00, 4.01],
                        [821, 3.21, 4.01],
                        [892, 3.29, 3.96],
                        [984, 3.35, 3.97],
                        [1088, 3.5, 4.02],
                        [1216, 3.62, 4.15]
                        ])
    lam_ti = titanium[:, 0]  # nm
    n_ti = titanium[:, 1]
    k_ti = titanium[:, 2]
    nti = np.interp(lam_nm, lam_ti, n_ti)
    kti = np.interp(lam_nm, lam_ti, k_ti)
    
    # Nickel
    localpath = os.path.dirname(os.path.abspath(__file__))
    fnNickel = os.path.join(localpath, 'data',
                            'nickel_data_from_Palik_via_Bala_wvlNM_n_k.txt')
    vnickel = np.loadtxt(fnNickel, delimiter="\t", unpack=False, comments="#")
    lam_nickel = vnickel[:, 0]  # nm
    n_nickel = vnickel[:, 1]
    k_nickel = vnickel[:, 2]
    nnickel = np.interp(lam_nm, lam_nickel, n_nickel)
    knickel = np.interp(lam_nm, lam_nickel, k_nickel)
    
    # Compute the complex transmission
    # tCoef = np.zeros((Nmetal, ), dtype=complex)  # initialize
    # rCoef = np.zeros((Nmetal, ), dtype=complex)  # initialize

    # for ii in range(Nmetal):
    #     dni = t_Ni_vec[ii]
    #     dti = t_Ti_vec[ii]
    #     dpm = t_PMGI_vec[ii]
        
    #     nvec = np.array([1, 1, npmgi, nnickel-1j*knickel, nti-1j*kti,
    #                      n_substrate], dtype=complex)
    #     dvec = np.array([d0-dpm-dni-dti, dpm, dni, dti])
        
    #     # Choose polarization
    #     if(pol == 2):  # Mean of the two
    #         [dummy1, dummy2, rr0, tt0] = solver(nvec, dvec, theta,
    #                                             lam, False)
    #         [dummy1, dummy2, rr1, tt1] = solver(nvec, dvec, theta,
    #                                             lam, True)
    #         rr = (rr0+rr1)/2.
    #         tt = (tt0+tt1)/2.
    #     elif(pol == 0 or pol == 1):
    #         [dumm1, dummy2, rr, tt] = solver(nvec, dvec, theta, lam,
    #                                          bool(pol))
    #     else:
    #         raise ValueError('Wrong input value for polarization.')

    #     # Choose phase convention
    #     if not flagOPD:
    #         tCoef[ii] = np.conj(tt)  # Complex field transmission coef
    #         rCoef[ii] = np.conj(rr)  # Complex field reflection coef
    #     else:  # OPD phase convention
    #         tCoef[ii] = tt  # Complex field transmission coeffient
    #         rCoef[ii] = rr  # Complex field reflection coeffient
                
    # Compute the complex transmission
    tCoef = np.zeros((Ndiel, Nmetal), dtype=complex)  # initialize
    rCoef = np.zeros((Ndiel, Nmetal), dtype=complex)  # initialize
    for jj in range(Ndiel):
        dpm = t_PMGI_vec[jj]
        
        for ii in range(Nmetal):
            dni = t_Ni_vec[ii]
            dti = t_Ti_vec[ii]
            
            nvec = np.array([1, 1, npmgi, nnickel-1j*knickel, nti-1j*kti,
                              n_substrate], dtype=complex)
            dvec = np.array([d0-dpm-dni-dti, dpm, dni, dti])
            
            # Choose polarization
            if(pol == 2):  # Mean of the two
                [dummy1, dummy2, rr0, tt0] = solver(nvec, dvec, theta,
                                                    lam, False)
                [dummy1, dummy2, rr1, tt1] = solver(nvec, dvec, theta,
                                                    lam, True)
                rr = (rr0+rr1)/2.
                tt = (tt0+tt1)/2.
            elif(pol == 0 or pol == 1):
                [dumm1, dummy2, rr, tt] = solver(nvec, dvec, theta, lam,
                                                  bool(pol))
            else:
                raise ValueError('Wrong input value for polarization.')

            # Choose phase convention
            if not flagOPD:
                tCoef[jj, ii] = np.conj(tt)  # Complex field transmission coef
                rCoef[jj, ii] = np.conj(rr)  # Complex field reflection coef
            else:  # OPD phase convention
                tCoef[jj, ii] = tt  # Complex field transmission coeffient
                rCoef[jj, ii] = rr  # Complex field reflection coeffient
    
    return tCoef, rCoef