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