Source code for protis.simulation

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Benjamin Vial
# This file is part of protis
# License: GPLv3
# See the documentation at protis.gitlab.io


from . import backend as bk
from . import get_block
from .eig import gen_eig
from .fft import *
from .plot import *
from .reduced import gram_schmidt
from .utils import *


[docs] class Simulation: def __init__(self, lattice, k=(0, 0), epsilon=1, mu=1, nh=100): self.lattice = lattice self.k = k self.epsilon = epsilon self.mu = mu self.nh0 = int(nh) nh0 = int(nh) if nh0 == 1: nh0 = 2 # Get the harmonics self.harmonics, self.nh = self.lattice.get_harmonics(nh0, sort=True) # Check if nh and resolution satisfy Nyquist criteria maxN = bk.max(self.harmonics) if self.lattice.discretization[0] <= 2 * maxN or ( self.lattice.discretization[1] <= 2 * maxN and not self.lattice.is_1D ): raise ValueError(f"lattice discretization must be > {2*maxN}") # Buid lattice vectors self.k = k self.build_epsilon_hat() self.build_mu_hat() @property def kx(self): r = self.lattice.reciprocal return ( self.k[0] + r[0, 0] * self.harmonics[0, :] + r[0, 1] * self.harmonics[1, :] ) @property def ky(self): r = self.lattice.reciprocal return ( self.k[1] + r[0, 1] * self.harmonics[0, :] + r[1, 1] * self.harmonics[1, :] ) @property def Kx(self): return bk.diag(self.kx) @property def Ky(self): return bk.diag(self.ky) def _get_toeplitz_matrix(self, u): if is_scalar(u): return u # else: # u *= bk.ones((self.nh,self.nh),dtype=bk.complex128) if is_anisotropic(u): utf = [ self._get_toeplitz_matrix(u[i, j]) for i in range(2) for j in range(2) ] utf.append(self._get_toeplitz_matrix(u[2, 2])) return block_z_anisotropic(*utf) uft = fourier_transform(u) ix = bk.arange(self.nh) jx, jy = bk.meshgrid(ix, ix, indexing="ij") delta = self.harmonics[:, jx] - self.harmonics[:, jy] return uft[delta[0, :], delta[1, :]]
[docs] def build_epsilon_hat(self): self.epsilon_hat = self._get_toeplitz_matrix(self.epsilon) return self.epsilon_hat
[docs] def build_mu_hat(self): self.mu_hat = self._get_toeplitz_matrix(self.mu) return self.mu_hat
[docs] def build_A(self, polarization): def matmuldiag(A, B): return bk.einsum("i,ik->ik", bk.array(bk.diag(A)), bk.array(B)) q = self.mu_hat if polarization == "TM" else self.epsilon_hat if is_scalar(q): A = 1 / q * (self.Kx @ self.Kx + self.Ky @ self.Ky) else: if is_anisotropic(q): if q.shape == (3, 3): u = bk.linalg.inv(q[:2, :2]) A = u[1, 1] * self.Kx @ self.Kx + u[0, 0] * self.Ky @ self.Ky A -= u[1, 0] * self.Kx @ self.Ky + u[0, 1] * self.Ky @ self.Kx else: q = block(q[:2, :2]) qxx = get_block(q, 0, 0, self.nh) qxy = get_block(q, 0, 1, self.nh) qyx = get_block(q, 1, 0, self.nh) qyy = get_block(q, 1, 1, self.nh) u = bk.linalg.inv(q) uxx = get_block(u, 0, 0, self.nh) uxy = get_block(u, 0, 1, self.nh) uyx = get_block(u, 1, 0, self.nh) uyy = get_block(u, 1, 1, self.nh) kyuxx = matmuldiag(self.Ky, uxx) kxuyy = matmuldiag(self.Kx, uyy) kxuyx = matmuldiag(self.Kx, uyx) kyuxy = matmuldiag(self.Ky, uxy) A = matmuldiag(self.Kx, kxuyy.T).T + matmuldiag(self.Ky, kyuxx.T).T A -= matmuldiag(self.Kx, kyuxy.T).T + matmuldiag(self.Ky, kxuyx.T).T else: u = bk.linalg.inv(q) kxu = matmuldiag(self.Kx, u) kyu = matmuldiag(self.Ky, u) A = matmuldiag(self.Kx.T, kxu.T).T + matmuldiag(self.Ky.T, kyu.T).T self.A = bk.array(A + 0j, dtype=bk.complex128) return self.A
[docs] def build_B(self, polarization): if polarization == "TM": if is_scalar(self.epsilon_hat): self.B = self.epsilon_hat else: self.B = ( self.epsilon_hat[2, 2] if is_anisotropic(self.epsilon_hat) else self.epsilon_hat ) elif is_scalar(self.mu_hat): self.B = self.mu_hat else: self.B = self.mu_hat[2, 2] if is_anisotropic(self.mu_hat) else self.mu_hat return self.B
[docs] def solve( self, polarization, vectors=True, rbme=None, return_square=False, sparse=False, neig=10, ktol=1e-12, reduced=False, **kwargs, ): self.build_A(polarization) self.build_B(polarization) if rbme is None: A = self.A B = self.B else: # reduced bloch mode expansion A = bk.conj(rbme.T) @ self.A @ rbme B = self.B if is_scalar(self.B) else bk.conj(rbme.T) @ self.B @ rbme self.A_red = A self.B_red = B eign = gen_eig(A, B, vectors=vectors, sparse=sparse, neig=neig, **kwargs) w = eign[0] if vectors else eign v = eign[1] if vectors else None lmin = bk.min( bk.linalg.norm( bk.array(self.lattice.basis_vectors, dtype=bk.float64), axis=0 ) ) kmin = 2 * bk.pi / lmin eps = ktol * kmin**2 k0 = (w + eps) ** 0.5 # this is to avoid nan gradients if return_square: i = bk.argsort(bk.real(w)) self.eigenvalues = w[i] else: i = bk.argsort(bk.real(k0)) self.eigenvalues = k0[i] if rbme is not None and vectors and not reduced: # retrieve full vectors v = rbme @ v @ bk.conj(rbme.T) self.eigenvectors = v[:, i] if vectors else None return (self.eigenvalues, self.eigenvectors) if vectors else self.eigenvalues
[docs] def get_rbme_matrix(self, N_RBME, bands_RBME, polarization): v = [] for kx, ky in bands_RBME: self.k = kx, ky self.solve(polarization, vectors=True) v.append(self.eigenvectors[:, :N_RBME]) U = bk.hstack(v) return gram_schmidt(U)
[docs] def unit_cell_integ(self, u): x, y = self.lattice.grid return bk.trapz(bk.trapz(u, y[0, :], axis=1), x[:, 0], axis=0)
[docs] def phasor(self): x, y = self.lattice.grid kx, ky = self.k return bk.exp(1j * (kx * x + ky * y))
[docs] def get_chi(self, polarization): q = self.epsilon if polarization == "TM" else self.mu if is_scalar(q): return q else: return q[2, 2] if is_anisotropic(q) else q
[docs] def get_xi(self, polarization): q = self.mu if polarization == "TM" else self.epsilon if is_scalar(q): return 1 / q else: return bk.linalg.inv(q[:2, :2]) if is_anisotropic(q) else 1 / q
[docs] def build_Cs(self, phi0, polarization): def matmuldiag(A, B): return bk.einsum("i,ik->ik", bk.array(bk.diag(A)), bk.array(B)) Kx = bk.array(self.Kx) + 0j Ky = bk.array(self.Ky) + 0j phi0 = bk.array(phi0) + 0j q = self.mu_hat if polarization == "TM" else self.epsilon_hat a = 1 / self.mu if polarization == "TM" else 1 / self.epsilon ahat = self._get_toeplitz_matrix(a) if is_scalar(q): Cs = 1 / q * Kx @ phi0, 1 / q * Ky @ phi0 else: if is_anisotropic(q): if q.shape == (3, 3): u = bk.linalg.inv(q[:2, :2]) Csy = -u[0, 0] * Ky + u[0, 1] * Kx Csx = u[1, 0] * Ky - u[1, 1] * Kx Cs = Csx @ phi0, Csy @ phi0 else: u = bk.linalg.inv(q) uxx = get_block(u, 0, 0, self.nh) uxy = get_block(u, 0, 1, self.nh) uyx = get_block(u, 1, 0, self.nh) uyy = get_block(u, 1, 1, self.nh) kyuxx = matmuldiag(Ky, uxx.T) kxuyy = matmuldiag(Kx, uyy.T) kxuxy = matmuldiag(Kx, uxy.T) kyuyx = matmuldiag(Ky, uyx.T) Csy = -kyuxx + kxuxy Csx = kyuyx - kxuyy Cs = Csx @ phi0, Csy @ phi0 else: u = bk.linalg.inv(q) kxu = matmuldiag(Kx, u.T).T kyu = matmuldiag(Ky, u.T).T ukx = matmuldiag(Kx, u) uky = matmuldiag(Ky, u) # # kxu = matmuldiag(u,Kx) # # kyu = matmuldiag(u,Ky) # # ukx = matmuldiag(u, Kx) # out = 1j * (-1 * Kx @ u -2*u @ Kx) @ phi0 # # out = 1j *(-1 * kxu +1*ukx) @ phi0 # x, y = self.lattice.grid # dx = x[1, 0] - x[0, 0] # dy = y[0, 1] - y[0, 0] # p = 1/self.mu if polarization == "TM" else 1/self.epsilon # dp = bk.gradient(p) # dp = dp[0] / dx # dphat = self._get_toeplitz_matrix(dp) # # out = (-2*1j * kxu - 1*dphat) @ phi0 # # out = (-bk.linalg.inv(dphat)) @ phi0 # # out = (- 1j*Kx@u) @ phi0 # Cs = out, -2 * 1j * kyu @ phi0 Cs = (kxu + ukx) @ phi0, kyu @ phi0 Cs = 1 * ahat @ (Kx @ phi0) + Kx @ (ahat @ phi0), kyu @ phi0 # Cs = -2 * 1j * u @ (Kx @ phi0) + 1j * (Kx @ u) @ phi0, kyu @ phi0 Cs = -1j * bk.stack(Cs).T return Cs
[docs] def normalization(self, mode, polarization): chi = self.get_chi(polarization) return self.unit_cell_integ(chi * mode * mode) ** 0.5
[docs] def coeff2mode(self, coeff): V = bk.zeros(self.lattice.discretization, dtype=bk.complex128) V = bk.array(V) V[self.harmonics[0], self.harmonics[1]] = bk.array(coeff) return inverse_fourier_transform(V)
[docs] def get_mode(self, imode): return self.coeff2mode(self.eigenvectors[:, imode])
[docs] def get_modes(self, imodes): return bk.stack([self.get_mode(imode) for imode in imodes]).T
[docs] def scalar_product_real(self, u, v): x, y = self.lattice.grid return bk.trapz(bk.trapz(self.epsilon.conj() * u.conj() * v, x[:, 0]), y[0])
[docs] def scalar_product_fourier(self, u, v): return (self.B @ u).conj() @ v.T
[docs] def scalar_product_rbme(self, u, v): return (self.B_red @ u).conj() @ v.T
[docs] def plot( self, field, nper=1, ax=None, cmap="RdBu_r", show_cell=False, cellstyle="-k", **kwargs, ): return plot2d( self.lattice, self.lattice.grid, field, nper, ax, cmap, show_cell, cellstyle, **kwargs, )
def _get_hom_tensor(self, phi0, phis1, polarization): # Kx = bk.array(self.Kx) + 0j # Ky = bk.array(self.Ky) + 0j phi1x, phi1y = phis1 x, y = self.lattice.grid dx = x[1, 0] - x[0, 0] dy = y[0, 1] - y[0, 0] dphi0 = bk.gradient(phi0) dphi0dx = dphi0[0] / dx dphi0dy = dphi0[1] / dy dphi1x = bk.gradient(phi1x) dphi1xdx = dphi1x[0] / dx dphi1xdy = dphi1x[1] / dy dphi1y = bk.gradient(phi1y) dphi1ydx = dphi1y[0] / dx dphi1ydy = dphi1y[1] / dy xi = self.get_xi(polarization) integxx = xi * (phi0**2 - dphi0dx * phi1x + dphi1xdx * phi0) Txx = self.unit_cell_integ(integxx) integyy = xi * (phi0**2 - dphi0dy * phi1y + dphi1ydy * phi0) Tyy = self.unit_cell_integ(integyy) integxy = xi * (-dphi0dx * phi1y + dphi1ydx * phi0) Txy = self.unit_cell_integ(integxy) integyx = xi * (-dphi0dy * phi1x + dphi1xdy * phi0) Tyx = self.unit_cell_integ(integyx) T = bk.zeros((2, 2), dtype=bk.complex128) T[0, 0] = Txx T[1, 1] = Tyy T[0, 1] = Txy T[1, 0] = Tyx return T
[docs] def get_hfh_tensor(self, imode, polarization): ph = self.phasor() k0 = self.eigenvalues[imode] coeffs0 = self.eigenvectors[:, imode] mode0 = self.coeff2mode(coeffs0) mode0 = mode0 * ph norma = self.normalization(mode0, polarization) mode0 = mode0 / norma coeffs0 = coeffs0 / norma Cs = self.build_Cs(coeffs0, polarization) M = -self.A + k0**2 * self.B * bk.array(bk.eye(self.nh)) coeffs1 = bk.linalg.solve(M, Cs) modes1 = [] for i in range(2): mode1 = self.coeff2mode(coeffs1[:, i]) mode1 *= ph modes1.append(mode1) T = self._get_hom_tensor(mode0, modes1, polarization) return bk.real(T)
[docs] def get_berry_curvature(self, kx, ky, eigenmode, method="fourier"): nkx = len(kx) nky = len(ky) if method == "fourier": scalar_product = self.scalar_product_fourier elif method == "real": scalar_product = self.scalar_product_real elif method == "rbme": scalar_product = self.scalar_product_rbme else: raise ValueError( f"Unknown method {method}: choose between 'fourier','real' or 'rbme'" ) phi = bk.empty((nkx - 1, nky - 1), dtype=bk.float64) for i in range(nkx - 1): for j in range(nky - 1): # print(i, j) u1 = eigenmode[i, j + 1] u2 = eigenmode[i + 1, j + 1] u3 = eigenmode[i + 1, j] u4 = eigenmode[i, j] p1 = scalar_product(u1, u2) p2 = scalar_product(u2, u3) p3 = scalar_product(u3, u4) p4 = scalar_product(u4, u1) q = p1 * p2 * p3 * p4 phi[i, j] = -bk.log(q).imag ds = (kx[1] - kx[0]) * (ky[1] - ky[0]) dkx = bk.array([kx[i + 1] - kx[i] for i in range(len(kx) - 1)]) dky = bk.array([ky[i + 1] - ky[i] for i in range(len(ky) - 1)]) return phi * dkx * dky
[docs] def get_chern_number(self, kx, ky, berry_curvature): dkx = bk.array([kx[i + 1] - kx[i] for i in range(len(kx) - 1)]) dky = bk.array([ky[i + 1] - ky[i] for i in range(len(ky) - 1)]) C = -bk.sum(berry_curvature / (dkx * dky)) / (2 * bk.pi) return C