def S_T(At, Bt): ''' function to create scattering matrices in the transmission regions different from S_layer because these regions only have one boundary condition to satisfy :param At: :param Bt: :return: ''' # assert type(At) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # assert type(Bt) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # S11 = (Bt) * np.linalg.inv(At); # S21 = 2*np.linalg.inv(At); # S12 = 0.5*(At - Bt * np.linalg.inv(At) * Bt); # S22 = - np.linalg.inv(At)*Bt S11 = (Bt) @ np.linalg.inv(At) S21 = 2 * np.linalg.inv(At) S12 = 0.5 * (At - Bt @ bslash(At, Bt)) S22 = -bslash(At, Bt) S_dict = { 'S11': S11, 'S22': S22, 'S12': S12, 'S21': S21 } S = np.block([[S11, S12], [S21, S22]]) return S, S_dict
def S_R(Ar, Br): ''' function to create scattering matrices in the reflection regions different from S_layer because these regions only have one boundary condition to satisfy :param Ar: :param Br: :return: ''' # assert type(Ar) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # assert type(Br) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # # S11 = -np.linalg.inv(Ar) * Br; # S12 = 2*np.linalg.inv(Ar); # S21 = 0.5*(Ar - Br * np.linalg.inv(Ar) * Br); # S22 = Br * np.linalg.inv(Ar) S11 = -bslash(Ar, Br) S12 = 2 * np.linalg.inv(Ar) S21 = 0.5 * (Ar - Br @ bslash(Ar, Br)) S22 = Br @ np.linalg.inv(Ar) S_dict = { 'S11': S11, 'S22': S22, 'S12': S12, 'S21': S21 } S = np.block([[S11, S12], [S21, S22]]) return S, S_dict
def P_matrix(Kx, Ky, e_conv, mu_conv): assert type(Kx) == np.ndarray, 'not array' assert type(Ky) == np.ndarray, 'not array' assert type(e_conv) == np.ndarray, 'not array' P = np.block( [[Kx @ bslash(e_conv, Ky), mu_conv - Kx @ bslash(e_conv, Kx)], [Ky @ bslash(e_conv, Ky) - mu_conv, -Ky @ bslash(e_conv, Kx)]]) return P
def RedhefferStar(SA, SB): #SA and SB are both 2x2 block matrices; ''' RedhefferStar for arbitrarily sized 2x2 block matrices for RCWA :param SA: dictionary containing the four sub-blocks :param SB: dictionary containing the four sub-blocks, keys are 'S11', 'S12', 'S21', 'S22' :return: ''' assert type(SA) == dict, 'not dict' assert type(SB) == dict, 'not dict' # once we break every thing like this, we should still have matrices SA_11 = SA['S11'] SA_12 = SA['S12'] SA_21 = SA['S21'] SA_22 = SA['S22'] SB_11 = SB['S11'] SB_12 = SB['S12'] SB_21 = SB['S21'] SB_22 = SB['S22'] N = len(SA_11) #SA_11 should be square so length is fine I = np.matrix(np.identity(N)) # D = np.linalg.inv(I-SB_11*SA_22); # F = np.linalg.inv(I-SA_22*SB_11); # # SAB_11 = SA_11 + SA_12*D*SB_11*SA_21; # SAB_12 = SA_12*D*SB_12; # SAB_21 = SB_21*F*SA_21; # SAB_22 = SB_22 + SB_21*F*SA_22*SB_12; D = (I - SB_11 @ SA_22) F = (I - SA_22 @ SB_11) SAB_11 = SA_11 + SA_12 @ bslash(D, SB_11) @ SA_21 SAB_12 = SA_12 @ bslash(D, SB_12) SAB_21 = SB_21 @ bslash(F, SA_21) SAB_22 = SB_22 + SB_21 @ bslash(F, SA_22) @ SB_12 SAB = np.block([[SAB_11, SAB_12], [SAB_21, SAB_22]]) SAB_dict = { 'S11': SAB_11, 'S22': SAB_22, 'S12': SAB_12, 'S21': SAB_21 } return SAB, SAB_dict
def Q_matrix(Kx, Ky, e_conv, mu_conv): ''' pressently assuming non-magnetic material so mu_conv = I :param Kx: now a matrix (NM x NM) :param Ky: now a matrix :param e_conv: (NM x NM) matrix containing the 2d convmat :return: ''' assert type(Kx) == np.ndarray, 'not array' assert type(Ky) == np.ndarray, 'not array' assert type(e_conv) == np.ndarray, 'not array' return np.block( [[Kx @ bslash(mu_conv, Ky), e_conv - Kx @ bslash(mu_conv, Kx)], [Ky @ bslash(mu_conv, Ky) - e_conv, -Ky @ bslash(mu_conv, Kx)]])
def B(W_layer, Wg, V_layer, Vg): #MINUS SIGN ''' :param W_layer: layer E-modes :param Wg: gap E-field modes :param V_layer: layer H_modes :param Vg: gap H-field modes # the numbering is just 1 and 2 because the order differs if we're in the structure # or outsid eof it :return: ''' # assert type(W_layer) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # assert type(Wg) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # assert type(V_layer) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # assert type(Vg) == np.matrixlib.defmatrix.matrix, 'not np.matrix' #B = np.linalg.inv(W_layer) * Wg - np.linalg.inv(V_layer) * Vg; B = bslash(W_layer, Wg) - bslash(V_layer, Vg) return B
def A(W_layer, Wg, V_layer, Vg): # PLUS SIGN ''' OFFICIAL EMLAB prescription inv(W_layer)*W_gap :param W_layer: layer E-modes :param Wg: gap E-field modes :param V_layer: layer H_modes :param Vg: gap H-field modes # the numbering is just 1 and 2 because the order differs if we're in the structure # or outsid eof it :return: ''' # assert type(W_layer) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # assert type(Wg) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # assert type(V_layer) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # assert type(Vg) == np.matrixlib.defmatrix.matrix, 'not np.matrix' #A = np.linalg.inv(W_layer) * Wg + np.linalg.inv(V_layer) * Vg; A = bslash(W_layer, Wg) + bslash(V_layer, Vg) return A
def S_layer(A, B, Li, k0, modes): ''' function to create scatter matrix in the ith layer of the uniform layer structure we assume that gap layers are used so we need only one A and one B :param A: function A = :param B: function B :param k0 #free -space wavevector magnitude (normalization constant) in Si Units :param Li #length of ith layer (in Si units) :param modes, eigenvalue matrix :return: S (4x4 scatter matrix) and Sdict, which contains the 2x2 block matrix as a dictionary ''' # assert type(A) == np.matrixlib.defmatrix.matrix, 'not np.matrix' # assert type(B) == np.matrixlib.defmatrix.matrix, 'not np.matrix' #sign convention (EMLAB is exp(-1i*k\dot r)) X_i = np.diag(np.exp(-np.diag(modes) * Li * k0)) #never use expm #term1 = (A - X_i * B * A.I * X_i * B).I # S11 = term1 * (X_i * B * A.I * X_i * A - B); # S12 = term1 * (X_i) * (A - B * A.I * B); # S22 = S11; # S21 = S12; term1 = (A - X_i @ B @ bslash(A, X_i) @ B) S11 = bslash(term1, (X_i @ B @ bslash(A, X_i) @ A - B)) S12 = bslash(term1, (X_i) @ (A - B @ bslash(A, B))) S22 = S11 S21 = S12 S_dict = { 'S11': S11, 'S22': S22, 'S12': S12, 'S21': S21 } S = np.block([[S11, S12], [S21, S22]]) return S, S_dict
## IMPLEMENT SCALING: these are the fourier orders of the x-direction decomposition. KX = np.diag((k_xi/k0)); #singular since we have a n=0, m= 0 order and incidence is normal # PQ_block = np.block([[zeros, np.linalg.inv(E_conv_inv)],[KX@bslash(E, KX) - I, zeros]]) # # plt.imshow(np.abs(PQ_block)); # # plt.show(); # print('condition of PQ block: '+str(np.linalg.cond(PQ_block))) # big_eigenvals, bigW = LA.eig(PQ_block); # print((bigW.shape, big_eigenvals.shape)) # Wp = bigW[0:PQ, PQ:] # plt.imshow(abs(bigW)) # plt.show(); ## construct matrix of Gamma^2 ('constant' term in ODE): ## one thing that isn't obvious is that are we doing element by element division or is it matricial B = (KX@bslash(Ezz, KX) - I); bE = np.linalg.inv(E_conv_inv) + bslash(Ezz,(Exz@Ezx)); #/Ezz; G = j*bslash(Ezz,Ezx) @ KX; H = j*KX @bslash(Ezz, Exz); #print((G,H)) print((bE.shape,G.shape, H.shape)) print((np.linalg.cond(B), np.linalg.cond(bE))) M = np.linalg.inv(bE); K = -(B + [email protected](bE)@G); C = -np.linalg.inv(bE)@G - [email protected](bE); Z = np.zeros_like(M); I = np.eye(M.shape[0], M.shape[1]); OA = np.block([[M, Z],[Z, I]]) OB = np.block(np.block([[C, K],[-I, Z]]))
## Region 2; transmission n2 = 1 #from the kx_components given the indices and wvln #2 * np.pi * np.arange(-N_p, N_p + 1) / (k0 * a_x) indices = np.arange(-num_ord, num_ord + 1) kx_array = (n1 * np.sin(theta_inc) + indices * (lam0 / lattice_constant)) #0 is one of them, k0*lam0 = 2*pi ## IMPLEMENT SCALING: these are the fourier orders of the x-direction decomposition. KX = np.diag(kx_array) #singular since we have a n=0, m= 0 order and incidence is normal ## construct matrix of Gamma^2 ('constant' term in ODE): #use fast fourier factorization rules here... Q = I - KX @ bslash(E_conv, KX) PQ = -bslash(E_conv_inv, Q) ## ================================================================================================## eigenvals, W = LA.eig(PQ) #A is NOT HERMITIAN #we should be gauranteed that all eigenvals are REAL eigenvals = eigenvals.astype('complex') lambda_matrix = np.diag((np.sqrt((eigenvals)))) ## THIS NEGATIVE SIGN IS CRUCIAL, but I'm not sure why V = -em.eigen_V(Q, W, lambda_matrix) kz_inc = n1 ## ================================================================================================## ## scattering matrix needed for 'gap medium'
## =====================STRUCTURE======================## ## Region I: reflected region (half space) n1 = 1;#cmath.sqrt(-1)*1e-12; #apparently small complex perturbations are bad in Region 1, these shouldn't be necessary ## Region 2; transmitted region n2 = 1; #from the kx_components given the indices and wvln kx_array = k0*(n1*np.sin(theta) + indices*(lam0 / lattice_constant)); #0 is one of them, k0*lam0 = 2*pi k_xi = kx_array; ## IMPLEMENT SCALING: these are the fourier orders of the x-direction decomposition. KX = np.diag((k_xi/k0)); #singular since we have a n=0, m= 0 order and incidence is normal ## construct matrix of Gamma^2 ('constant' term in ODE): A = bslash(E_conv_inv,KX*bslash(E, KX) - I); #conditioning of this matrix is not bad, A SHOULD BE SYMMETRIC A = bslash(E_conv_inv,KX*bslash(E, KX) - I); #conditioning of this matrix is not bad, A SHOULD BE SYMMETRIC #sum of a symmetric matrix and a diagonal matrix should be symmetric; ## # when we calculate eigenvals, how do we know the eigenvals correspond to each particular fourier order? eigenvals, W = LA.eigh(A); #A should be symmetric or hermitian #we should be gauranteed that all eigenvals are REAL eigenvals = eigenvals.astype('complex'); Q = np.diag(np.sqrt(eigenvals)); #Q should only be positive square root of eigenvals V = bslash(E,W)@Q; #H modes ## this is the great typo which has killed us all this time X = np.diag(np.exp(-k0*np.diag(Q)*d)); #this is poorly conditioned because exponentiation ## pointwise exponentiation vs exponentiating a matrix
def RWCA_1D_TM(E, E_conv_inv, lattice_constant, theta, num_ord, wavelength_scan,d): ''' :param E: [e] :param E_conv_inv: [1/e] :param lattice_constant: :param theta: :param num_ord: :param wavelength_scan: :return: ''' I = np.identity(2 * num_ord + 1) indices = np.arange(-num_ord, num_ord + 1); spectra_R = []; spectra_T = []; #arrays to hold reflectivity and transmissitivity PQ = 2*num_ord+1; # E is now the convolution of fourier amplitudes for wvlen in wavelength_scan: j = cmath.sqrt(-1); lam0 = wvlen; k0 = 2 * np.pi / lam0; #free space wavelength in SI units print('wavelength: ' + str(wvlen)); ## =====================STRUCTURE======================## ## Region I: reflected region (half space) n1 = 1;#cmath.sqrt(-1)*1e-12; #apparently small complex perturbations are bad in Region 1, these shouldn't be necessary ## Region 2; transmitted region n2 = 1; #from the kx_components given the indices and wvln kx_array = k0*(n1*np.sin(theta) + indices*(lam0 / lattice_constant)); #0 is one of them, k0*lam0 = 2*pi k_xi = kx_array; ## IMPLEMENT SCALING: these are the fourier orders of the x-direction decomposition. KX = np.diag((k_xi/k0)); #singular since we have a n=0, m= 0 order and incidence is normal ## construct matrix of Gamma^2 ('constant' term in ODE): A = np.linalg.inv(E_conv_inv)@(KX@bslash(E, KX) - I); #conditioning of this matrix is not bad, A SHOULD BE SYMMETRIC # when we calculate eigenvals, how do we know the eigenvals correspond to each particular fourier order? #eigenvals, W = LA.eigh(A); #A should be symmetric or hermitian, which won't be the case in the TM mode eigenvals, W = LA.eig(A); #we should be gauranteed that all eigenvals are REAL eigenvals = eigenvals.astype('complex'); Q = np.diag(np.sqrt(eigenvals)); #Q should only be positive square root of eigenvals V = E_conv_inv@(W@Q); #H modes ## this is the great typo which has killed us all this time X = np.diag(np.exp(-k0*np.diag(Q)*d)); #this is poorly conditioned because exponentiation ## pointwise exponentiation vs exponentiating a matrix ## observation: almost everything beyond this point is worse conditioned k_I = k0**2*(n1**2 - (k_xi/k0)**2); #k_z in reflected region k_I,zi k_II = k0**2*(n2**2 - (k_xi/k0)**2); #k_z in transmitted region k_I = k_I.astype('complex'); k_I = np.sqrt(k_I); k_II = k_II.astype('complex'); k_II = np.sqrt(k_II); Z_I = np.diag(k_I / (n1**2 * k0 )); Z_II = np.diag(k_II /(n2**2 * k0)); delta_i0 = np.zeros((len(kx_array),1)); delta_i0[num_ord] = 1; n_delta_i0 = delta_i0*j*np.cos(theta)/n1; ## design auxiliary variables: SEE derivation in notebooks: RCWA_note.ipynb # we want to design the computation to avoid operating with X, particularly with inverses # since X is the worst conditioned thing O = np.block([ [W, W], [V,-V] ]); #this is much better conditioned than S.. f = I; g = j * Z_II; #all matrices fg = np.concatenate((f,g),axis = 0) ab = np.matmul(np.linalg.inv(O),fg); a = ab[0:PQ,:]; b = ab[PQ:,:]; term = X @ a @ np.linalg.inv(b) @ X; f = W @ (I+term); g = V@(-I+term); T = np.linalg.inv(np.matmul(j*Z_I, f) + g); T = np.dot(T, (np.dot(j*Z_I, delta_i0) + n_delta_i0)); R = np.dot(f,T)-delta_i0; #shouldn't change T = np.dot(np.matmul(np.linalg.inv(b),X),T) ## calculate diffraction efficiencies #I would expect this number to be real... DE_ri = R*np.conj(R)*np.real(np.expand_dims(k_I,1))/(k0*n1*np.cos(theta)); DE_ti = T*np.conj(T)*np.real(np.expand_dims(k_II,1)/n2**2)/(k0*np.cos(theta)/n1); #print(np.sum(DE_ri)) spectra_R.append(np.sum(DE_ri)); #spectra_T.append(T); spectra_T.append(np.sum(DE_ti)) return spectra_R, spectra_T;
n1 = 1 #cmath.sqrt(-1)*1e-12; #apparently small complex perturbations are bad in Region 1, these shouldn't be necessary ## Region 2; transmitted region n2 = 1 #from the kx_components given the indices and wvln kx_array = k0 * (n1 * np.sin(theta) + indices * (lam0 / lattice_constant)) #0 is one of them, k0*lam0 = 2*pi k_xi = kx_array ## IMPLEMENT SCALING: these are the fourier orders of the x-direction decomposition. KX = np.diag((k_xi / k0)) #singular since we have a n=0, m= 0 order and incidence is normal ## construct matrix of Gamma^2 ('constant' term in ODE): A = np.linalg.inv(E_conv_inv) @ (KX @ bslash(E, KX) - I) #conditioning of this matrix is not bad, A SHOULD BE SYMMETRIC #sum of a symmetric matrix and a diagonal matrix should be symmetric; ## # when we calculate eigenvals, how do we know the eigenvals correspond to each particular fourier order? #eigenvals, W = LA.eigh(A); #A should be symmetric or hermitian, which won't be the case in the TM mode eigenvals, W = LA.eig(A) #we should be gauranteed that all eigenvals are REAL eigenvals = eigenvals.astype('complex') Q = np.diag(np.sqrt(eigenvals)) #Q should only be positive square root of eigenvals V = E_conv_inv @ (W @ Q) #H modes