Пример #1
0
def AB_matrices(omega, g, eps_array, d_array, chi_array=None, pol='TE'):
    """
    Function to calculate A,B coeff
    Output: array of shape [M+1,2]
    """
    assert len(d_array)==len(eps_array)-2, \
        'd_array should have length = num_layers'
    if chi_array is None:
        chi_array = chi(omega, g, eps_array)

    if pol.lower()=='te':
        S_matrices, T_matrices = \
                S_T_matrices_TE(omega, g, eps_array, d_array)
    elif pol.lower()=='tm':
        S_matrices, T_matrices = \
                S_T_matrices_TM(omega, g, eps_array, d_array)
    else:
        raise Exception("Polarization should be 'TE' or 'TM'.")
    A0 = 0
    B0 = 1 
    AB0 = bd.array([A0, B0]).reshape(-1,1)

    # A, B coeff for each layer
    ABs = [AB0, bd.dot(T_matrices[0], bd.dot(S_matrices[0], AB0))]
    for i,S in enumerate(S_matrices[1:]):
        term = bd.dot(S_matrices[i+1], bd.dot(T_matrices[i], ABs[-1]))
        if i < len(S_matrices)-2:
            term = bd.dot(T_matrices[i+1], term)
        ABs.append(term)
    return bd.array(ABs)
Пример #2
0
    def __init__(self, *args):
        """
        Initialize a Bravais lattice.
        If a single argument is passed, then

            - 'square': initializes a square lattice.
            - 'hexagonal': initializes a hexagonal lattice.

        with lattice constant a = 1 in both cases.

        If two arguments are passed, they should each be 2-element arrays
        defining the elementary vectors of the lattice.
        """

        # Primitive vectors cell definition
        (a1, a2) = self._parse_input(*args)
        self.a1 = a1[0:2]
        self.a2 = a2[0:2]

        ec_area = bd.norm(bd.cross(a1, a2))
        a3 = bd.array([0, 0, 1])

        # Reciprocal lattice basis vectors
        b1 = 2*np.pi*bd.cross(a2, a3)/bd.dot(a1, bd.cross(a2, a3)) 
        b2 = 2*np.pi*bd.cross(a3, a1)/bd.dot(a2, bd.cross(a3, a1))

        bz_area = bd.norm(bd.cross(b1, b2))

        self.b1 = b1[0:2]
        self.b2 = b2[0:2]

        self.ec_area = ec_area  # Elementary cell area
        self.bz_area = bz_area  # Brillouin zone area
Пример #3
0
def D22_TE(omega, g, eps_array, d_array):
    """
    Function to get TE guided modes by solving D22=0
    Input
        omega           : frequency * 2π , in units of light speed/unit length
        g               : wave vector along propagation direction
        eps_array       : shape[M+1,1], slab permittivities
        d_array         : thicknesses of each layer
    Output
        D_22
    """
    S_matrices, T_matrices = S_T_matrices_TE(omega, g, eps_array, d_array)
    D = S_matrices[0, :, :]
    for i, S in enumerate(S_matrices[1:]):
        T = T_matrices[i]
        D = bd.dot(S, bd.dot(T, bd.dot(T, D)))
    return D[1, 1]
Пример #4
0
    def run(self, kpoints=np.array([[0], [0]]), pol='te', numeig=10):
        """
        Run the simulation. The computed eigen-frequencies are stored in
        :attr:`PlaneWaveExp.freqs`, and the corresponding eigenvectors - 
        in :attr:`PlaneWaveExp.eigvecs`.
        
        Parameters
        ----------
        kpoints : np.ndarray, optional
            Numpy array of shape (2, Nk) with the [kx, ky] coordinates of the 
            k-vectors over which the simulation is run.
        pol : {'te', 'tm'}, optional
            Polarization of the modes.
        numeig : int, optional
            Number of eigen-frequencies to be stored (starting from lowest).
        """
         
        self._kpoints = kpoints
        self.pol = pol.lower()
        # Change this if switching to a solver that allows for variable numeig
        self.numeig = numeig

        self._compute_ft()
        self._compute_eps_inv()

        freqs = []
        self._eigvecs = []
        for ik, k in enumerate(kpoints.T):
            # Construct the matrix for diagonalization
            if self.pol == 'te':
                mat = bd.dot(bd.transpose(k[:, bd.newaxis] + self.gvec), 
                                (k[:, bd.newaxis] + self.gvec))
                mat = mat * self.eps_inv_mat 
                
            elif self.pol == 'tm':
                Gk = bd.sqrt(bd.square(k[0] + self.gvec[0, :]) + \
                        bd.square(k[1] + self.gvec[1, :]))
                mat = bd.outer(Gk, Gk)
                mat = mat * self.eps_inv_mat
            else:
                raise ValueError("Polarization should be 'TE' or 'TM'")

            # Diagonalize using numpy.linalg.eigh() for now; should maybe switch 
            # to scipy.sparse.linalg.eigsh() in the future
            # NB: we shift the matrix by np.eye to avoid problems at the zero-
            # frequency mode at Gamma
            (freq2, evecs) = bd.eigh(mat + bd.eye(mat.shape[0]))
            freq1 = bd.sqrt(bd.abs(freq2 - bd.ones(mat.shape[0])))/2/np.pi
            i_sort = bd.argsort(freq1)[0:self.numeig]
            freq = freq1[i_sort]
            evec = evecs[:, i_sort]
            freqs.append(freq)
            self._eigvecs.append(evec)

        # Store the eigenfrequencies taking the standard reduced frequency 
        # convention for the units (2pi a/c)    
        self._freqs = bd.array(freqs)
        self.mat = mat
Пример #5
0
def D22(omega, g, eps_array, d_array, pol='TM'):
    """
    Function to get TE guided modes by solving D22=0
    Input
        omega           : frequency * 2π , in units of light speed/unit length
        g               : wave vector along propagation direction
        eps_array       : shape[M+1,1], slab permittivities
        d_array         : thicknesses of each layer
    Output
        D_22
    """
    if eps_array.size == 3:
        (eps1, eps2, eps3) = [e for e in eps_array]
        # (chis1, chis2, chis3) = [chi(omega, g, e) for e in eps_array]
        (chis1, chis2, chis3) = chis_3layer(omega, g, eps_array)

        tcos = -1j*bd.cos(chis2*d_array)
        tsin = -bd.sin(chis2*d_array)

        if pol.lower() == 'te':
            D22 = chis2*(chis1 + chis3)*tcos + \
                    (chis1*chis3 + bd.square(chis2))*tsin
        elif pol.lower() == 'tm':    
            D22 = chis2/eps2*(chis1/eps1 + chis3/eps3)*tcos + \
                    (chis1/eps1*chis3/eps3 + bd.square(chis2/eps2))*tsin
        return D22
    else:
        if pol.lower() == 'te':
            S_mat, T_mat = S_T_matrices_TE(omega, g, eps_array, d_array)
        elif pol.lower() == 'tm':
            S_mat, T_mat = S_T_matrices_TM(omega, g, eps_array, d_array)
        else:
            raise ValueError("Polarization should be 'TE' or 'TM'.")

        D = S_mat[0,:,:]
        for i,S in enumerate(S_mat[1:]):
            T = T_mat[i]
            D = bd.dot(S, bd.dot(T, bd.dot(T, D)))
        return D[1,1]
Пример #6
0
    def rotate(self, angle):
        """Rotate the polygon around its center of mass by `angle` radians
        """

        rotmat = bd.array([[bd.cos(angle), -bd.sin(angle)], \
                            [bd.sin(angle), bd.cos(angle)]])
        (xj, yj) = (bd.array(self.x_edges), bd.array(self.y_edges))
        com_x = bd.sum((xj + bd.roll(xj, -1)) * (xj * bd.roll(yj, -1) - \
                    bd.roll(xj, -1) * yj))/6/self.area
        com_y = bd.sum((yj + bd.roll(yj, -1)) * (xj * bd.roll(yj, -1) - \
                    bd.roll(xj, -1) * yj))/6/self.area
        new_coords = bd.dot(rotmat, bd.vstack((xj-com_x, yj-com_y)))

        self.x_edges = new_coords[0, :] + com_x
        self.y_edges = new_coords[1, :] + com_y

        return self
Пример #7
0
    def compute_ft(self, gvec):
        """Compute Fourier transform of the polygon

        The polygon is assumed to take a value of 1 inside and a value of 0 
        outside.

        The Fourier transform calculation follows that of Lee, IEEE TAP (1984).

        Parameters
        ----------
        gvec : np.ndarray of shape (2, Ng)
            g-vectors at which FT is evaluated
        """
        (gx, gy) = self._parse_ft_gvec(gvec)

        (xj, yj) = self.x_edges, self.y_edges
        npts = xj.shape[0]
        ng = gx.shape[0]
        # Note: the paper uses +1j*g*x convention for FT while we use 
        # -1j*g*x everywhere in legume
        gx = -gx[:, bd.newaxis]
        gy = -gy[:, bd.newaxis]
        xj = xj[bd.newaxis, :]
        yj = yj[bd.newaxis, :]

        ft = bd.zeros((ng), dtype=bd.complex);

        aj = (bd.roll(xj, -1, axis=1) - xj + 1e-10) / \
                (bd.roll(yj, -1, axis=1) - yj + 1e-20)
        bj = xj - aj * yj

        # We first handle the Gx = 0 case
        ind_gx0 = np.abs(gx[:, 0]) < 1e-10
        ind_gx = ~ind_gx0
        if np.sum(ind_gx0) > 0:
            # And first the Gy = 0 case
            ind_gy0 = np.abs(gy[:, 0]) < 1e-10
            if np.sum(ind_gy0*ind_gx0) > 0:
                ft = ind_gx0*ind_gy0*bd.sum(xj * bd.roll(yj, -1, axis=1)-\
                                yj * bd.roll(xj, -1, axis=1))/2
                # Remove the Gx = 0, Gy = 0 component
                ind_gx0[ind_gy0] = False

            # Compute the remaining Gx = 0 components
            a2j = 1 / aj
            b2j = yj - a2j * xj
            bgtemp = gy * b2j
            agtemp1 = bd.dot(gx, xj) + bd.dot(gy, a2j * xj)
            agtemp2 = bd.dot(gx, bd.roll(xj, -1, axis=1)) + \
                    bd.dot(gy, a2j * bd.roll(xj, -1, axis=1))
            denom = gy * (gx + bd.dot(gy, a2j))
            ftemp = bd.sum(bd.exp(1j*bgtemp) * (bd.exp(1j*agtemp2) - \
                    bd.exp(1j*agtemp1)) * \
                    denom / (bd.square(denom) + 1e-50) , axis=1)
            ft = bd.where(ind_gx0, ftemp, ft)

        # Finally compute the general case for Gx != 0
        if np.sum(ind_gx) > 0:
            bgtemp = bd.dot(gx, bj)
            agtemp1 = bd.dot(gy, yj) + bd.dot(gx, aj * yj)
            agtemp2 = bd.dot(gy, bd.roll(yj, -1, axis=1)) + \
                        bd.dot(gx, aj * bd.roll(yj, -1, axis=1))
            denom = gx * (gy + bd.dot(gx, aj))
            ftemp = -bd.sum(bd.exp(1j*bgtemp) * (bd.exp(1j * agtemp2) - \
                    bd.exp(1j * agtemp1)) * \
                    denom / (bd.square(denom) + 1e-50) , axis=1)
            ft = bd.where(ind_gx, ftemp, ft)

        return ft
Пример #8
0
def rad_modes(omega: float, g_array: np.ndarray, eps_array: np.ndarray,
            d_array: np.ndarray, pol: str='TE', clad: int=0):
    """ 
    Function to compute the radiative modes of a multi-layer structure
    Input
    g_array         : numpy array of wave vector amplitudes 
    eps_array       : numpy array of slab permittivities, starting with lower 
                      cladding and ending with upper cladding

    d_array         : thicknesses of each layer
    omega           : frequency of the radiative mode
    pol             : polarization, 'te' or 'tm'
    clad            : radiating into cladding index, 0 (lower) or 1 (upper)
    Output
    Xs, Ys          : X, Y coefficients of the modes in every layer
    """

    Xs, Ys = [], []
    for ig, g in enumerate(g_array):
        g_val = max([g, 1e-10])
        # Get the scattering and transfer matrices
        if pol.lower()=='te' and clad==0:
            S_mat, T_mat = S_T_matrices_TE(omega, g_val, eps_array[::-1], 
                            d_array[::-1])
        elif pol.lower()=='te' and clad==1:
            S_mat, T_mat = S_T_matrices_TE(omega, g_val, eps_array, d_array)
        elif pol.lower()=='tm' and clad==0:
            S_mat, T_mat = S_T_matrices_TM(omega, g_val, eps_array[::-1], 
                            d_array[::-1])
        elif pol.lower()=='tm' and clad==1:
            S_mat, T_mat = S_T_matrices_TM(omega, g_val, eps_array, d_array)
        
        # Compute the transfer matrix coefficients
        coeffs = [bd.array([0, 1])]
        coeffs.append(bd.dot(T_mat[0], bd.dot(S_mat[0], coeffs[0])))
        for i, S in enumerate(S_mat[1:-1]):
            T2 = T_mat[i+1]
            T1 = T_mat[i]
            coeffs.append(bd.dot(T2, bd.dot(S, bd.dot(T1, coeffs[-1]))))
        coeffs.append(bd.dot(S_mat[-1], bd.dot(T_mat[-1], coeffs[-1])))
        coeffs = bd.array(coeffs, dtype=bd.complex).transpose()

        # Normalize
        coeffs = coeffs / coeffs[1, -1] 
        if pol=='te':
            c_ind = [0, -1]
            coeffs = coeffs/bd.sqrt(eps_array[c_ind[clad]])/omega
        # Assign correctly based on which cladding the modes radiate to
        if clad == 0:
            Xs.append(coeffs[0, ::-1].ravel())
            Ys.append(coeffs[1, ::-1].ravel())
        elif clad == 1:
            Xs.append(coeffs[1, :].ravel())
            Ys.append(coeffs[0, :].ravel())

    Xs = bd.array(Xs, dtype=bd.complex).transpose()
    Ys = bd.array(Ys, dtype=bd.complex).transpose()

    # Fix the dimension if g_array is an empty list
    if len(g_array)==0:
        Xs = bd.ones((eps_array.size, 1))*Xs
        Ys = bd.ones((eps_array.size, 1))*Ys

    """
    (Xs, Ys) corresponds to the X, W coefficients for TE radiative modes in 
    Andreani and Gerace PRB (2006), and to the Z, Y coefficients for TM modes

    Note that there's an error in the manuscript; within our definitions, the 
    correct statement should be: X3 = 0 for states out-going in the lower 
    cladding; normalize through W1; and W1 = 0 for states out-going in the upper
    cladding; normalize through X3.
    """
    return (Xs, Ys)
Пример #9
0
    def ft_field_xy(self, field, kind, mind):
        """
        Compute the 'H', 'D' or 'E' field Fourier components in the xy-plane.
        
        Parameters
        ----------
        field : {'H', 'D', 'E'}
            The field to be computed. 
        kind : int
            The field of the mode at `PlaneWaveExp.kpoints[:, kind]` is 
            computed.
        mind : int
            The field of the `mind` mode at that kpoint is computed.

        Note
        ----
        The function outputs 1D arrays with the same size as 
        `PlaneWaveExp.gvec[0, :]` corresponding to the G-vectors in 
        that array.
        
        Returns
        -------
        fi_x : np.ndarray
            The Fourier transform of the x-component of the specified field. 
        fi_y : np.ndarray
            The Fourier transform of the y-component of the specified field. 
        fi_z : np.ndarray
            The Fourier transform of the z-component of the specified field. 
        """
        evec = self.eigvecs[kind][:, mind]
        omega = self.freqs[kind][mind]*2*np.pi
        k = self.kpoints[:, kind]

        # G + k vectors
        gkx = self.gvec[0, :] + k[0] + 1e-10
        gky = self.gvec[1, :] + k[1]
        gnorm = bd.sqrt(bd.square(gkx) + bd.square(gky))

        # Unit vectors in the propagation direction
        px = gkx / gnorm
        py = gky / gnorm

        # Unit vectors in-plane orthogonal to the propagation direction
        qx = py
        qy = -px

        if field.lower()=='h':
            if self.pol == 'te':
                Hx_ft = bd.zeros(gnorm.shape)
                Hy_ft = bd.zeros(gnorm.shape)
                Hz_ft = evec

            elif self.pol == 'tm':
                Hx_ft = evec * qx
                Hy_ft = evec * qy
                Hz_ft = bd.zeros(gnorm.shape)

            return (Hx_ft, Hy_ft, Hz_ft)

        elif field.lower()=='d' or field.lower()=='e':
            if self.pol == 'te':
                Dx_ft = 1j / omega * evec * qx
                Dy_ft = 1j / omega * evec * qy
                Dz_ft = bd.zeros(gnorm.shape)

            elif self.pol == 'tm':
                Dx_ft = bd.zeros(gnorm.shape)
                Dy_ft = bd.zeros(gnorm.shape)
                Dz_ft = 1j / omega * evec

            if field.lower()=='d':
                return (Dx_ft, Dy_ft, Dz_ft)
            else:
                # Get E-field by convolving FT(1/eps) with FT(D)
                Ex_ft = bd.dot(self.eps_inv_mat, Dx_ft)
                Ey_ft = bd.dot(self.eps_inv_mat, Dy_ft)
                Ez_ft = bd.dot(self.eps_inv_mat, Dz_ft)
                return (Ex_ft, Ey_ft, Ez_ft)