├── .bumpversion.cfg ├── .dir-locals.el ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── pypi.yml │ ├── python-app.yml │ └── release.yml ├── .gitignore ├── CHANGES ├── EMpy ├── RCWA.py ├── __init__.py ├── constants.py ├── devices.py ├── materials.py ├── modesolvers │ ├── FD.py │ ├── FMM.py │ ├── __init__.py │ ├── geometries.py │ └── interface.py ├── scattering.py ├── transfer_matrix.py ├── utils.py └── version.py ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── examples ├── ex_APRR.py ├── ex_NRR.py ├── ex_RCWA.py ├── ex_RCWA_2.py ├── ex_RCWA_3.py ├── ex_SRR.py ├── ex_laser_etch_monitor.py ├── ex_modesolver.py ├── ex_modesolver_2.py ├── ex_modesolver_3.py ├── ex_modesolver_4.py ├── ex_modesolver_dch.py ├── ex_modesolver_dch_anis.py ├── ex_transfer_matrix.py ├── ex_transfer_matrix_2.py ├── ex_transfer_matrix_2.py.png ├── ex_transfer_matrix_3.py ├── lc_data.dat └── nk.py ├── mypy.ini ├── requirements.txt ├── requirements_dev.in ├── requirements_dev.txt ├── scripts └── FDTD.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── materials_test.py └── utils_test.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | files = EMpy/version.py 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | current_version = 2.2.1 7 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;;; Directory Local Variables 2 | ;;; For more information see (info "(emacs) Directory Variables") 3 | 4 | ((python-mode . ((flycheck-pycheckers-max-line-length . 130) 5 | (eval . (blacken-mode t))))) 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | groups: 9 | github-actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: weekly 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 12 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPi 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | publish: 10 | name: Build and publish Python 🐍 distributions 📦 to PyPI 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Set up Python 3.10 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.10" 18 | - name: Install pypa/build 19 | run: >- 20 | python -m 21 | pip install 22 | build 23 | --user 24 | - name: Build a binary wheel and a source tarball 25 | run: >- 26 | python -m 27 | build 28 | --sdist 29 | --wheel 30 | --outdir dist/ 31 | . 32 | - name: Publish distribution 📦 to PyPI 33 | if: startsWith(github.ref, 'refs/tags') 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | with: 36 | password: ${{ secrets.PYPI_API_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests, and lint with a single version of Python 2 | # For more information, see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | python-version: ["3.10", "3.11", "3.12", "3.13"] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: "${{ matrix.python-version }}" 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install flake8 mypy pytest 32 | pip install -r requirements.txt 33 | pip install --editable . 34 | - name: Lint with flake8 35 | run: | 36 | flake8 EMpy tests examples scripts 37 | - name: Lint with pyflakes 38 | run: | 39 | pyflakes EMpy tests examples scripts 40 | - name: Lint with mypy 41 | run: | 42 | mypy EMpy tests examples scripts 43 | - name: Run tests 44 | run: | 45 | pytest 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Release 15 | uses: softprops/action-gh-release@v2 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | *.egg 4 | .eggs/ 5 | .tox/ 6 | .vscode/ 7 | dist 8 | build 9 | .idea 10 | .DS_Store -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 1.2 2 | === 3 | 4 | * Fix #14 anisotropic epsfunc 5 | * Documentation fixes 6 | * PEP8 fixes 7 | * BSD license 8 | 9 | 1.1 10 | === 11 | 12 | * Allow to specify an arbitrary function as refractive index. -------------------------------------------------------------------------------- /EMpy/RCWA.py: -------------------------------------------------------------------------------- 1 | """Rigorous Coupled Wave Analysis. 2 | 3 | The algorithm, described in Glytsis, "Three-dimensional (vector) 4 | rigorous coupled-wave analysis of anisotropic grating diffraction", 5 | JOSA A, 7(8), 1990, permits to find the diffraction efficiencies 6 | (i.e. the power normalized to unity) reflected and transmitted by a 7 | multilayer. 8 | The multilayer can be made of isotropic or anisotropic layers and 9 | binary gratings. 10 | Two versions of the algorithm are present: an isotropic one, stable 11 | for every diffraction order and layer thickness, and an anisotropic 12 | one, only stable for low diffraction orders and thin layers. 13 | 14 | """ 15 | __author__ = "Lorenzo Bolla" 16 | 17 | import numpy as np 18 | from scipy.linalg import toeplitz, inv, eig, solve as linsolve 19 | from numpy import pi 20 | 21 | from EMpy.utils import ( 22 | cond, 23 | warning, 24 | BinaryGrating, 25 | SymmetricDoubleGrating, 26 | AsymmetricDoubleGrating, 27 | ) 28 | 29 | 30 | def dispersion_relation_ordinary(kx, ky, k, nO): 31 | """Dispersion relation for the ordinary wave. 32 | 33 | NOTE 34 | See eq. 15 in Glytsis, "Three-dimensional (vector) rigorous 35 | coupled-wave analysis of anisotropic grating diffraction", 36 | JOSA A, 7(8), 1990 Always give positive real or negative 37 | imaginary. 38 | """ 39 | 40 | if kx.shape != ky.shape: 41 | raise ValueError("kx and ky must have the same length") 42 | 43 | delta = (k * nO) ** 2 - (kx**2 + ky**2) 44 | kz = np.sqrt(delta) 45 | 46 | # Adjust sign of real/imag part 47 | kz.real = abs(kz.real) 48 | kz.imag = -abs(kz.imag) 49 | 50 | return kz 51 | 52 | 53 | def dispersion_relation_extraordinary(kx, ky, k, nO, nE, c): 54 | """Dispersion relation for the extraordinary wave. 55 | 56 | NOTE 57 | See eq. 16 in Glytsis, "Three-dimensional (vector) rigorous 58 | coupled-wave analysis of anisotropic grating diffraction", 59 | JOSA A, 7(8), 1990 Always give positive real or negative 60 | imaginary. 61 | """ 62 | 63 | if kx.shape != ky.shape or c.size != 3: 64 | raise ValueError( 65 | "kx and ky must have the same length and c must have 3 components" 66 | ) 67 | 68 | kz = np.empty_like(kx) 69 | 70 | for ii in range(0, kx.size): 71 | alpha = nE**2 - nO**2 72 | beta = kx[ii] / k * c[0] + ky[ii] / k * c[1] 73 | 74 | # coeffs 75 | C = np.array( 76 | [ 77 | nO**2 + c[2] ** 2 * alpha, 78 | 2.0 * c[2] * beta * alpha, 79 | nO**2 * (kx[ii] ** 2 + ky[ii] ** 2) / k**2 80 | + alpha * beta**2 81 | - nO**2 * nE**2, 82 | ] 83 | ) 84 | 85 | # two solutions of type +x or -x, purely real or purely imag 86 | tmp_kz = k * np.roots(C) 87 | 88 | # get the negative imaginary part or the positive real one 89 | if np.any(np.isreal(tmp_kz)): 90 | kz[ii] = np.absolute(tmp_kz[0]) 91 | else: 92 | kz[ii] = -1j * np.absolute(tmp_kz[0]) 93 | 94 | return kz 95 | 96 | 97 | class RCWA: 98 | 99 | """Class to handle the RCWA solvers. 100 | 101 | NOTE 102 | See Glytsis, "Three-dimensional (vector) rigorous coupled-wave 103 | analysis of anisotropic grating diffraction", JOSA A, 7(8), 1990 104 | 105 | The following variables, used throughout the code, have the following 106 | meaning: 107 | 108 | alpha: float 109 | angle between wave vector k1 and xy plane, in radians 110 | 111 | delta: float 112 | angle between the y axis and the projection of k1 onto the xy 113 | plane, in radians 114 | 115 | psi: angle between the D vector of the plane wave and the xy plane, 116 | in radians, TM: 0, TE: numpy.pi / 2 117 | 118 | phi: angle between the grating vector K and y axis (in the xy plane), 119 | in radians, the grating is modulated in the direction of the 120 | grating vector. 121 | 122 | """ 123 | 124 | def __init__(self, multilayer, alpha, delta, psi, phi, n): 125 | """Set the multilayer, the angles of incidence and the diffraction order. 126 | 127 | INPUT 128 | multilayer = Multilayer obj describing the sequence of layers. 129 | alpha, delta, psi, phi = angles of the incident wave (in 130 | radiant). 131 | n = orders of diffractions to retain in the computation. 132 | """ 133 | self.setMultilayer(multilayer) 134 | self.LAMBDA = self.get_pitch() 135 | self.alpha = alpha 136 | self.delta = delta 137 | self.psi = psi 138 | self.phi = phi 139 | self.n = n 140 | 141 | def setMultilayer(self, m): 142 | """Set the multilayer, simplifying it first.""" 143 | self.multilayer = m.simplify() 144 | 145 | def get_pitch(self): 146 | """Inspect the multilayer to check that all the binary 147 | gratings present have the same pitch, and return it.""" 148 | idx = np.where( 149 | [ 150 | ( 151 | isinstance(m, BinaryGrating) 152 | | isinstance(m, SymmetricDoubleGrating) 153 | | isinstance(m, AsymmetricDoubleGrating) 154 | ) 155 | for m in self.multilayer 156 | ] 157 | )[0] 158 | if idx.size == 0: 159 | # warning('no BinaryGratings: better use a simple transfer matrix.') 160 | return 1.0 # return LAMBDA: any value will do 161 | else: 162 | # check that all the pitches are the same! 163 | l = np.asarray([self.multilayer[i].pitch for i in idx]) 164 | if not np.all(l == l[0]): 165 | raise ValueError("All the BinaryGratings must have the same pitch.") 166 | else: 167 | return l[0] 168 | 169 | 170 | class IsotropicRCWA(RCWA): 171 | 172 | """Isotropic RCWA solver.""" 173 | 174 | def solve(self, wls): 175 | """Isotropic solver. 176 | 177 | INPUT 178 | wls = wavelengths to scan (any asarray-able object). 179 | 180 | OUTPUT 181 | self.DE1, self.DE3 = power reflected and transmitted. 182 | 183 | NOTE 184 | see: 185 | Moharam, "Formulation for stable and efficient implementation 186 | of the rigorous coupled-wave analysis of binary gratings", 187 | JOSA A, 12(5), 1995 188 | Lalanne, "Highly improved convergence of the coupled-wave 189 | method for TM polarization", JOSA A, 13(4), 1996 190 | Moharam, "Stable implementation of the rigorous coupled-wave 191 | analysis for surface-relief gratings: enhanced trasmittance 192 | matrix approach", JOSA A, 12(5), 1995 193 | """ 194 | 195 | self.wls = np.atleast_1d(wls) 196 | 197 | LAMBDA = self.LAMBDA 198 | n = self.n 199 | multilayer = self.multilayer 200 | alpha = self.alpha 201 | delta = self.delta 202 | psi = self.psi 203 | phi = self.phi 204 | 205 | nlayers = len(multilayer) 206 | i = np.arange(-n, n + 1) 207 | nood = 2 * n + 1 208 | hmax = nood - 1 209 | 210 | # grating vector (on the xz plane) 211 | # grating on the xy plane 212 | K = 2 * pi / LAMBDA * np.array([np.sin(phi), 0.0, np.cos(phi)], dtype=complex) 213 | 214 | DE1 = np.zeros((nood, self.wls.size)) 215 | DE3 = np.zeros_like(DE1) 216 | 217 | dirk1 = np.array( 218 | [np.sin(alpha) * np.cos(delta), np.sin(alpha) * np.sin(delta), np.cos(alpha)] 219 | ) 220 | 221 | # usefull matrices 222 | I = np.eye(i.size) 223 | I2 = np.eye(i.size * 2) 224 | ZERO = np.zeros_like(I) 225 | 226 | X = np.zeros((2 * nood, 2 * nood, nlayers), dtype=complex) 227 | MTp1 = np.zeros((2 * nood, 2 * nood, nlayers), dtype=complex) 228 | MTp2 = np.zeros_like(MTp1) 229 | 230 | EPS2 = np.zeros(2 * hmax + 1, dtype=complex) 231 | EPS21 = np.zeros_like(EPS2) 232 | 233 | dlt = (i == 0).astype(int) 234 | 235 | for iwl, wl in enumerate(self.wls): 236 | # free space wavevector 237 | k = 2 * pi / wl 238 | 239 | n1 = multilayer[0].mat.n(wl).item() 240 | n3 = multilayer[-1].mat.n(wl).item() 241 | 242 | # incident plane wave wavevector 243 | k1 = k * n1 * dirk1 244 | 245 | # all the other wavevectors 246 | tmp_x = k1[0] - i * K[0] 247 | tmp_y = k1[1] * np.ones_like(i) 248 | tmp_z = dispersion_relation_ordinary(tmp_x, tmp_y, k, n1) 249 | k1i = np.r_[[tmp_x], [tmp_y], [tmp_z]] 250 | 251 | # k2i = np.r_[[k1[0] - i*K[0]], [k1[1] - i * K[1]], [-i * K[2]]] 252 | 253 | tmp_z = dispersion_relation_ordinary(tmp_x, tmp_y, k, n3) 254 | k3i = np.r_[[k1i[0, :]], [k1i[1, :]], [tmp_z]] 255 | 256 | # aliases for constant wavevectors 257 | kx = k1i[0, :] 258 | ky = k1[1] 259 | 260 | # angles of reflection 261 | # phi_i = np.arctan2(ky,kx) 262 | phi_i = np.arctan2(ky, kx.real) # OKKIO 263 | 264 | Kx = np.diag(kx / k) 265 | Ky = ky / k * I 266 | Z1 = np.diag(k1i[2, :] / (k * n1**2)) 267 | Y1 = np.diag(k1i[2, :] / k) 268 | Z3 = np.diag(k3i[2, :] / (k * n3**2)) 269 | Y3 = np.diag(k3i[2, :] / k) 270 | # Fc = np.diag(np.cos(phi_i)) 271 | fc = np.cos(phi_i) 272 | # Fs = np.diag(np.sin(phi_i)) 273 | fs = np.sin(phi_i) 274 | 275 | MR = np.asarray( 276 | np.bmat([[I, ZERO], [-1j * Y1, ZERO], [ZERO, I], [ZERO, -1j * Z1]]) 277 | ) 278 | 279 | MT = np.asarray( 280 | np.bmat([[I, ZERO], [1j * Y3, ZERO], [ZERO, I], [ZERO, 1j * Z3]]) 281 | ) 282 | 283 | # internal layers (grating or layer) 284 | X.fill(0.0) 285 | MTp1.fill(0.0) 286 | MTp2.fill(0.0) 287 | for nlayer in range(nlayers - 2, 0, -1): # internal layers 288 | layer = multilayer[nlayer] 289 | d = layer.thickness 290 | 291 | EPS2, EPS21 = layer.getEPSFourierCoeffs(wl, n, anisotropic=False) 292 | 293 | E = toeplitz(EPS2[hmax::-1], EPS2[hmax:]) 294 | E1 = toeplitz(EPS21[hmax::-1], EPS21[hmax:]) 295 | E11 = inv(E1) 296 | # B = np.dot(Kx, linsolve(E,Kx)) - I 297 | B = kx[:, np.newaxis] / k * linsolve(E, Kx) - I 298 | # A = np.dot(Kx, Kx) - E 299 | A = np.diag((kx / k) ** 2) - E 300 | 301 | # Note: solution bug alfredo 302 | # randomizzo Kx un po' a caso finche' cond(A) e' piccolo (<1e10) 303 | # soluzione sporca... :-( 304 | # per certi kx, l'operatore di helmholtz ha 2 autovalori nulli e A, B 305 | # non sono invertibili --> cambio leggermente i kx... ma dovrei invece 306 | # trattare separatamente (analiticamente) questi casi 307 | if cond(A) > 1e10: 308 | warning("BAD CONDITIONING: randomization of kx") 309 | while cond(A) > 1e10: 310 | Kx = Kx * (1 + 1e-9 * np.rand()) 311 | B = kx[:, np.newaxis] / k * linsolve(E, Kx) - I 312 | A = np.diag((kx / k) ** 2) - E 313 | 314 | if np.absolute(K[2] / k) > 1e-10: 315 | raise ValueError( 316 | "First Order Helmholtz Operator not implemented, yet!" 317 | ) 318 | 319 | elif ky == 0 or np.allclose(np.diag(Ky / ky * k), 1): 320 | # lalanne 321 | # H_U_reduced = np.dot(Ky, Ky) + A 322 | H_U_reduced = (ky / k) ** 2 * I + A 323 | # H_S_reduced = np.dot(Ky, Ky) + np.dot(Kx, linsolve(E, np.dot(Kx, E11))) - E11 324 | H_S_reduced = ( 325 | (ky / k) ** 2 * I 326 | + kx[:, np.newaxis] / k * linsolve(E, kx[:, np.newaxis] / k * E11) 327 | - E11 328 | ) 329 | 330 | q1, W1 = eig(H_U_reduced) 331 | q1 = np.sqrt(q1) 332 | q2, W2 = eig(H_S_reduced) 333 | q2 = np.sqrt(q2) 334 | 335 | # boundary conditions 336 | 337 | # V11 = np.dot(linsolve(A, W1), np.diag(q1)) 338 | V11 = linsolve(A, W1) * q1[np.newaxis, :] 339 | V12 = (ky / k) * np.dot(linsolve(A, Kx), W2) 340 | V21 = (ky / k) * np.dot(linsolve(B, Kx), linsolve(E, W1)) 341 | # V22 = np.dot(linsolve(B, W2), np.diag(q2)) 342 | V22 = linsolve(B, W2) * q2[np.newaxis, :] 343 | 344 | # Vss = np.dot(Fc, V11) 345 | Vss = fc[:, np.newaxis] * V11 346 | # Wss = np.dot(Fc, W1) + np.dot(Fs, V21) 347 | Wss = fc[:, np.newaxis] * W1 + fs[:, np.newaxis] * V21 348 | # Vsp = np.dot(Fc, V12) - np.dot(Fs, W2) 349 | Vsp = fc[:, np.newaxis] * V12 - fs[:, np.newaxis] * W2 350 | # Wsp = np.dot(Fs, V22) 351 | Wsp = fs[:, np.newaxis] * V22 352 | # Wpp = np.dot(Fc, V22) 353 | Wpp = fc[:, np.newaxis] * V22 354 | # Vpp = np.dot(Fc, W2) + np.dot(Fs, V12) 355 | Vpp = fc[:, np.newaxis] * W2 + fs[:, np.newaxis] * V12 356 | # Wps = np.dot(Fc, V21) - np.dot(Fs, W1) 357 | Wps = fc[:, np.newaxis] * V21 - fs[:, np.newaxis] * W1 358 | # Vps = np.dot(Fs, V11) 359 | Vps = fs[:, np.newaxis] * V11 360 | 361 | Mc2bar = np.asarray( 362 | np.bmat( 363 | [ 364 | [Vss, Vsp, Vss, Vsp], 365 | [Wss, Wsp, -Wss, -Wsp], 366 | [Wps, Wpp, -Wps, -Wpp], 367 | [Vps, Vpp, Vps, Vpp], 368 | ] 369 | ) 370 | ) 371 | 372 | x = np.r_[np.exp(-k * q1 * d), np.exp(-k * q2 * d)] 373 | 374 | # Mc1 = np.dot(Mc2bar, np.diag(np.r_[np.ones_like(x), x])) 375 | xx = np.r_[np.ones_like(x), x] 376 | Mc1 = Mc2bar * xx[np.newaxis, :] 377 | 378 | X[:, :, nlayer] = np.diag(x) 379 | 380 | MTp = linsolve(Mc2bar, MT) 381 | MTp1[:, :, nlayer] = MTp[0 : 2 * nood, :] 382 | MTp2 = MTp[2 * nood :, :] 383 | 384 | MT = np.dot( 385 | Mc1, 386 | np.r_[ 387 | I2, 388 | np.dot(MTp2, linsolve(MTp1[:, :, nlayer], X[:, :, nlayer])), 389 | ], 390 | ) 391 | 392 | else: 393 | ValueError("Second Order Helmholtz Operator not implemented, yet!") 394 | 395 | # M = np.asarray(np.bmat([-MR, MT])) 396 | M = np.c_[-MR, MT] 397 | b = np.r_[ 398 | np.sin(psi) * dlt, 399 | 1j * np.sin(psi) * n1 * np.cos(alpha) * dlt, 400 | -1j * np.cos(psi) * n1 * dlt, 401 | np.cos(psi) * np.cos(alpha) * dlt, 402 | ] 403 | 404 | x = linsolve(M, b) 405 | R, T = np.split(x, 2) 406 | Rs, Rp = np.split(R, 2) 407 | for ii in range(1, nlayers - 1): 408 | T = np.dot(linsolve(MTp1[:, :, ii], X[:, :, ii]), T) 409 | Ts, Tp = np.split(T, 2) 410 | 411 | DE1[:, iwl] = (k1i[2, :] / (k1[2])).real * np.absolute(Rs) ** 2 + ( 412 | k1i[2, :] / (k1[2] * n1**2) 413 | ).real * np.absolute(Rp) ** 2 414 | DE3[:, iwl] = (k3i[2, :] / (k1[2])).real * np.absolute(Ts) ** 2 + ( 415 | k3i[2, :] / (k1[2] * n3**2) 416 | ).real * np.absolute(Tp) ** 2 417 | 418 | # save the results 419 | self.DE1 = DE1 420 | self.DE3 = DE3 421 | 422 | return self 423 | 424 | # def plot(self): 425 | # """Plot the diffraction efficiencies.""" 426 | # g = Gnuplot.Gnuplot() 427 | # g('set xlabel "$\lambda$"') 428 | # g('set ylabel "diffraction efficiency"') 429 | # g('set yrange [0:1]') 430 | # g('set data style linespoints') 431 | # g.plot(Gnuplot.Data(self.wls, self.DE1[self.n,:], with_ = 'linespoints', title = 'DE1'), \ 432 | # Gnuplot.Data(self.wls, self.DE3[self.n,:], with_ = 'linespoints', title = 'DE3')) 433 | # raw_input('press enter to close the graph...') 434 | 435 | def __str__(self): 436 | return ( 437 | "ISOTROPIC RCWA SOLVER\n\n%s\n\nLAMBDA = %g\nalpha = %g\ndelta = %g\npsi = %g\nphi = %g\nn = %d" 438 | % ( 439 | self.multilayer.__str__(), 440 | self.LAMBDA, 441 | self.alpha, 442 | self.delta, 443 | self.psi, 444 | self.phi, 445 | self.n, 446 | ) 447 | ) 448 | 449 | 450 | class AnisotropicRCWA(RCWA): 451 | 452 | """Anisotropic RCWA solver.""" 453 | 454 | def solve(self, wls): 455 | """Anisotropic solver. 456 | 457 | INPUT 458 | wls = wavelengths to scan (any asarray-able object). 459 | 460 | OUTPUT 461 | self.DEO1, self.DEE1, self.DEO3, self.DEE3 = power reflected 462 | and transmitted. 463 | """ 464 | 465 | self.wls = np.atleast_1d(wls) 466 | 467 | LAMBDA = self.LAMBDA 468 | n = self.n 469 | multilayer = self.multilayer 470 | alpha = self.alpha 471 | delta = self.delta 472 | psi = self.psi 473 | phi = self.phi 474 | 475 | nlayers = len(multilayer) 476 | i = np.arange(-n, n + 1) 477 | nood = 2 * n + 1 478 | hmax = nood - 1 479 | 480 | DEO1 = np.zeros((nood, self.wls.size)) 481 | DEO3 = np.zeros_like(DEO1) 482 | DEE1 = np.zeros_like(DEO1) 483 | DEE3 = np.zeros_like(DEO1) 484 | 485 | c1 = np.array([1.0, 0.0, 0.0]) 486 | c3 = np.array([1.0, 0.0, 0.0]) 487 | # grating on the xy plane 488 | K = 2 * pi / LAMBDA * np.array([np.sin(phi), 0.0, np.cos(phi)], dtype=complex) 489 | dirk1 = np.array( 490 | [np.sin(alpha) * np.cos(delta), np.sin(alpha) * np.sin(delta), np.cos(alpha)] 491 | ) 492 | 493 | # D polarization vector 494 | u = np.array( 495 | [ 496 | np.cos(psi) * np.cos(alpha) * np.cos(delta) - np.sin(psi) * np.sin(delta), 497 | np.cos(psi) * np.cos(alpha) * np.sin(delta) + np.sin(psi) * np.cos(delta), 498 | -np.cos(psi) * np.sin(alpha), 499 | ] 500 | ) 501 | 502 | kO1i = np.zeros((3, i.size), dtype=complex) 503 | kE1i = np.zeros_like(kO1i) 504 | kO3i = np.zeros_like(kO1i) 505 | kE3i = np.zeros_like(kO1i) 506 | 507 | Mp = np.zeros((4 * nood, 4 * nood, nlayers), dtype=complex) 508 | M = np.zeros((4 * nood, 4 * nood, nlayers), dtype=complex) 509 | 510 | dlt = (i == 0).astype(int) 511 | 512 | for iwl, wl in enumerate(self.wls): 513 | nO1 = nE1 = multilayer[0].mat.n(wl).item() 514 | nO3 = nE3 = multilayer[-1].mat.n(wl).item() 515 | 516 | # wavevectors 517 | k = 2 * pi / wl 518 | 519 | eps1 = np.diag(np.asarray([nE1, nO1, nO1]) ** 2) 520 | eps3 = np.diag(np.asarray([nE3, nO3, nO3]) ** 2) 521 | 522 | # ordinary wave 523 | abskO1 = k * nO1 524 | # abskO3 = k * nO3 525 | # extraordinary wave 526 | # abskE1 = k * nO1 *nE1 / np.sqrt(nO1**2 + (nE1**2 - nO1**2) * np.dot(-c1, dirk1)**2) 527 | # abskE3 = k * nO3 *nE3 / np.sqrt(nO3**2 + (nE3**2 - nO3**2) * np.dot(-c3, dirk1)**2) 528 | 529 | k1 = abskO1 * dirk1 530 | 531 | kO1i[0, :] = k1[0] - i * K[0] 532 | kO1i[1, :] = k1[1] * np.ones_like(i) 533 | kO1i[2, :] = -dispersion_relation_ordinary(kO1i[0, :], kO1i[1, :], k, nO1) 534 | 535 | kE1i[0, :] = kO1i[0, :] 536 | kE1i[1, :] = kO1i[1, :] 537 | kE1i[2, :] = -dispersion_relation_extraordinary( 538 | kE1i[0, :], kE1i[1, :], k, nO1, nE1, c1 539 | ) 540 | 541 | kO3i[0, :] = kO1i[0, :] 542 | kO3i[1, :] = kO1i[1, :] 543 | kO3i[2, :] = dispersion_relation_ordinary(kO3i[0, :], kO3i[1, :], k, nO3) 544 | 545 | kE3i[0, :] = kO1i[0, :] 546 | kE3i[1, :] = kO1i[1, :] 547 | kE3i[2, :] = dispersion_relation_extraordinary( 548 | kE3i[0, :], kE3i[1, :], k, nO3, nE3, c3 549 | ) 550 | 551 | # k2i = np.r_[[k1[0] - i * K[0]], [k1[1] - i * K[1]], [k1[2] - i * K[2]]] 552 | k2i = np.r_[[k1[0] - i * K[0]], [k1[1] - i * K[1]], [-i * K[2]]] 553 | 554 | # aliases for constant wavevectors 555 | kx = kO1i[0, :] # o kE1i(1,;), tanto e' lo stesso 556 | ky = k1[1] 557 | 558 | # matrices 559 | I = np.eye(nood, dtype=complex) 560 | ZERO = np.zeros((nood, nood), dtype=complex) 561 | Kx = np.diag(kx / k) 562 | Ky = ky / k * I 563 | Kz = np.diag(k2i[2, :] / k) 564 | KO1z = np.diag(kO1i[2, :] / k) 565 | KE1z = np.diag(kE1i[2, :] / k) 566 | KO3z = np.diag(kO3i[2, :] / k) 567 | KE3z = np.diag(kE3i[2, :] / k) 568 | 569 | ARO = Kx * eps1[0, 0] + Ky * eps1[1, 0] + KO1z * eps1[2, 0] 570 | BRO = Kx * eps1[0, 1] + Ky * eps1[1, 1] + KO1z * eps1[2, 1] 571 | CRO_1 = inv(Kx * eps1[0, 2] + Ky * eps1[1, 2] + KO1z * eps1[2, 2]) 572 | 573 | ARE = Kx * eps1[0, 0] + Ky * eps1[1, 0] + KE1z * eps1[2, 0] 574 | BRE = Kx * eps1[0, 1] + Ky * eps1[1, 1] + KE1z * eps1[2, 1] 575 | CRE_1 = inv(Kx * eps1[0, 2] + Ky * eps1[1, 2] + KE1z * eps1[2, 2]) 576 | 577 | ATO = Kx * eps3[0, 0] + Ky * eps3[1, 0] + KO3z * eps3[2, 0] 578 | BTO = Kx * eps3[0, 1] + Ky * eps3[1, 1] + KO3z * eps3[2, 1] 579 | CTO_1 = inv(Kx * eps3[0, 2] + Ky * eps3[1, 2] + KO3z * eps3[2, 2]) 580 | 581 | ATE = Kx * eps3[0, 0] + Ky * eps3[1, 0] + KE3z * eps3[2, 0] 582 | BTE = Kx * eps3[0, 1] + Ky * eps3[1, 1] + KE3z * eps3[2, 1] 583 | CTE_1 = inv(Kx * eps3[0, 2] + Ky * eps3[1, 2] + KE3z * eps3[2, 2]) 584 | 585 | DRE = c1[1] * KE1z - c1[2] * Ky 586 | ERE = c1[2] * Kx - c1[0] * KE1z 587 | FRE = c1[0] * Ky - c1[1] * Kx 588 | 589 | DTE = c3[1] * KE3z - c3[2] * Ky 590 | ETE = c3[2] * Kx - c3[0] * KE3z 591 | FTE = c3[0] * Ky - c3[1] * Kx 592 | 593 | b = np.r_[ 594 | u[0] * dlt, 595 | u[1] * dlt, 596 | (k1[1] / k * u[2] - k1[2] / k * u[1]) * dlt, 597 | (k1[2] / k * u[0] - k1[0] / k * u[2]) * dlt, 598 | ] 599 | Ky_CRO_1 = ky / k * CRO_1 600 | Ky_CRE_1 = ky / k * CRE_1 601 | Kx_CRO_1 = kx[:, np.newaxis] / k * CRO_1 602 | Kx_CRE_1 = kx[:, np.newaxis] / k * CRE_1 603 | MR31 = -np.dot(Ky_CRO_1, ARO) 604 | MR32 = -np.dot(Ky_CRO_1, BRO) - KO1z 605 | MR33 = -np.dot(Ky_CRE_1, ARE) 606 | MR34 = -np.dot(Ky_CRE_1, BRE) - KE1z 607 | MR41 = np.dot(Kx_CRO_1, ARO) + KO1z 608 | MR42 = np.dot(Kx_CRO_1, BRO) 609 | MR43 = np.dot(Kx_CRE_1, ARE) + KE1z 610 | MR44 = np.dot(Kx_CRE_1, BRE) 611 | MR = np.asarray( 612 | np.bmat( 613 | [ 614 | [I, ZERO, I, ZERO], 615 | [ZERO, I, ZERO, I], 616 | [MR31, MR32, MR33, MR34], 617 | [MR41, MR42, MR43, MR44], 618 | ] 619 | ) 620 | ) 621 | 622 | Ky_CTO_1 = ky / k * CTO_1 623 | Ky_CTE_1 = ky / k * CTE_1 624 | Kx_CTO_1 = kx[:, np.newaxis] / k * CTO_1 625 | Kx_CTE_1 = kx[:, np.newaxis] / k * CTE_1 626 | MT31 = -np.dot(Ky_CTO_1, ATO) 627 | MT32 = -np.dot(Ky_CTO_1, BTO) - KO3z 628 | MT33 = -np.dot(Ky_CTE_1, ATE) 629 | MT34 = -np.dot(Ky_CTE_1, BTE) - KE3z 630 | MT41 = np.dot(Kx_CTO_1, ATO) + KO3z 631 | MT42 = np.dot(Kx_CTO_1, BTO) 632 | MT43 = np.dot(Kx_CTE_1, ATE) + KE3z 633 | MT44 = np.dot(Kx_CTE_1, BTE) 634 | MT = np.asarray( 635 | np.bmat( 636 | [ 637 | [I, ZERO, I, ZERO], 638 | [ZERO, I, ZERO, I], 639 | [MT31, MT32, MT33, MT34], 640 | [MT41, MT42, MT43, MT44], 641 | ] 642 | ) 643 | ) 644 | 645 | Mp.fill(0.0) 646 | M.fill(0.0) 647 | 648 | for nlayer in range(nlayers - 2, 0, -1): # internal layers 649 | layer = multilayer[nlayer] 650 | thickness = layer.thickness 651 | 652 | EPS2, EPS21 = layer.getEPSFourierCoeffs(wl, n, anisotropic=True) 653 | 654 | # Exx = np.squeeze(EPS2[0, 0, :]) 655 | # Exx = toeplitz(np.flipud(Exx[0:hmax + 1]), Exx[hmax:]) 656 | Exy = np.squeeze(EPS2[0, 1, :]) 657 | Exy = toeplitz(np.flipud(Exy[0 : hmax + 1]), Exy[hmax:]) 658 | Exz = np.squeeze(EPS2[0, 2, :]) 659 | Exz = toeplitz(np.flipud(Exz[0 : hmax + 1]), Exz[hmax:]) 660 | 661 | Eyx = np.squeeze(EPS2[1, 0, :]) 662 | Eyx = toeplitz(np.flipud(Eyx[0 : hmax + 1]), Eyx[hmax:]) 663 | Eyy = np.squeeze(EPS2[1, 1, :]) 664 | Eyy = toeplitz(np.flipud(Eyy[0 : hmax + 1]), Eyy[hmax:]) 665 | Eyz = np.squeeze(EPS2[1, 2, :]) 666 | Eyz = toeplitz(np.flipud(Eyz[0 : hmax + 1]), Eyz[hmax:]) 667 | 668 | Ezx = np.squeeze(EPS2[2, 0, :]) 669 | Ezx = toeplitz(np.flipud(Ezx[0 : hmax + 1]), Ezx[hmax:]) 670 | Ezy = np.squeeze(EPS2[2, 1, :]) 671 | Ezy = toeplitz(np.flipud(Ezy[0 : hmax + 1]), Ezy[hmax:]) 672 | Ezz = np.squeeze(EPS2[2, 2, :]) 673 | Ezz = toeplitz(np.flipud(Ezz[0 : hmax + 1]), Ezz[hmax:]) 674 | 675 | Exx_1 = np.squeeze(EPS21[0, 0, :]) 676 | Exx_1 = toeplitz(np.flipud(Exx_1[0 : hmax + 1]), Exx_1[hmax:]) 677 | Exx_1_1 = inv(Exx_1) 678 | 679 | # lalanne 680 | Ezz_1 = inv(Ezz) 681 | Ky_Ezz_1 = ky / k * Ezz_1 682 | Kx_Ezz_1 = kx[:, np.newaxis] / k * Ezz_1 683 | Exz_Ezz_1 = np.dot(Exz, Ezz_1) 684 | Eyz_Ezz_1 = np.dot(Eyz, Ezz_1) 685 | H11 = 1j * np.dot(Ky_Ezz_1, Ezy) 686 | H12 = 1j * np.dot(Ky_Ezz_1, Ezx) 687 | H13 = np.dot(Ky_Ezz_1, Kx) 688 | H14 = I - np.dot(Ky_Ezz_1, Ky) 689 | H21 = 1j * np.dot(Kx_Ezz_1, Ezy) 690 | H22 = 1j * np.dot(Kx_Ezz_1, Ezx) 691 | H23 = np.dot(Kx_Ezz_1, Kx) - I 692 | H24 = -np.dot(Kx_Ezz_1, Ky) 693 | H31 = np.dot(Kx, Ky) + Exy - np.dot(Exz_Ezz_1, Ezy) 694 | H32 = Exx_1_1 - np.dot(Ky, Ky) - np.dot(Exz_Ezz_1, Ezx) 695 | H33 = 1j * np.dot(Exz_Ezz_1, Kx) 696 | H34 = -1j * np.dot(Exz_Ezz_1, Ky) 697 | H41 = np.dot(Kx, Kx) - Eyy + np.dot(Eyz_Ezz_1, Ezy) 698 | H42 = -np.dot(Kx, Ky) - Eyx + np.dot(Eyz_Ezz_1, Ezx) 699 | H43 = -1j * np.dot(Eyz_Ezz_1, Kx) 700 | H44 = 1j * np.dot(Eyz_Ezz_1, Ky) 701 | H = 1j * np.diag(np.repeat(np.diag(Kz), 4)) + np.asarray( 702 | np.bmat( 703 | [ 704 | [H11, H12, H13, H14], 705 | [H21, H22, H23, H24], 706 | [H31, H32, H33, H34], 707 | [H41, H42, H43, H44], 708 | ] 709 | ) 710 | ) 711 | 712 | q, W = eig(H) 713 | W1, W2, W3, W4 = np.split(W, 4) 714 | 715 | # 716 | # boundary conditions 717 | # 718 | # x = [R T] 719 | # R = [ROx ROy REx REy] 720 | # T = [TOx TOy TEx TEy] 721 | # b + MR.R = M1p.c 722 | # M1.c = M2p.c 723 | # ... 724 | # ML.c = MT.T 725 | # therefore: b + MR.R = (M1p.M1^-1.M2p.M2^-1. ...).MT.T 726 | # missing equations from (46)..(49) in glytsis_rigorous 727 | # [b] = [-MR Mtot.MT] [R] 728 | # [0] [...........] [T] 729 | 730 | z = np.zeros_like(q) 731 | z[np.where(q.real > 0)] = -thickness 732 | D = np.exp(k * q * z) 733 | Sy0 = W1 * D[np.newaxis, :] 734 | Sx0 = W2 * D[np.newaxis, :] 735 | Uy0 = W3 * D[np.newaxis, :] 736 | Ux0 = W4 * D[np.newaxis, :] 737 | 738 | z = thickness * np.ones_like(q) 739 | z[np.where(q.real > 0)] = 0 740 | D = np.exp(k * q * z) 741 | D1 = np.exp(-1j * k2i[2, :] * thickness) 742 | Syd = D1[:, np.newaxis] * W1 * D[np.newaxis, :] 743 | Sxd = D1[:, np.newaxis] * W2 * D[np.newaxis, :] 744 | Uyd = D1[:, np.newaxis] * W3 * D[np.newaxis, :] 745 | Uxd = D1[:, np.newaxis] * W4 * D[np.newaxis, :] 746 | 747 | Mp[:, :, nlayer] = np.r_[Sx0, Sy0, -1j * Ux0, -1j * Uy0] 748 | M[:, :, nlayer] = np.r_[Sxd, Syd, -1j * Uxd, -1j * Uyd] 749 | 750 | Mtot = np.eye(4 * nood, dtype=complex) 751 | for nlayer in range(1, nlayers - 1): 752 | Mtot = np.dot(np.dot(Mtot, Mp[:, :, nlayer]), inv(M[:, :, nlayer])) 753 | 754 | BC_b = np.r_[b, np.zeros_like(b)] 755 | BC_A1 = np.c_[-MR, np.dot(Mtot, MT)] 756 | BC_A2 = np.asarray( 757 | np.bmat( 758 | [ 759 | [ 760 | (c1[0] * I - c1[2] * np.dot(CRO_1, ARO)), 761 | (c1[1] * I - c1[2] * np.dot(CRO_1, BRO)), 762 | ZERO, 763 | ZERO, 764 | ZERO, 765 | ZERO, 766 | ZERO, 767 | ZERO, 768 | ], 769 | [ 770 | ZERO, 771 | ZERO, 772 | (DRE - np.dot(np.dot(FRE, CRE_1), ARE)), 773 | (ERE - np.dot(np.dot(FRE, CRE_1), BRE)), 774 | ZERO, 775 | ZERO, 776 | ZERO, 777 | ZERO, 778 | ], 779 | [ 780 | ZERO, 781 | ZERO, 782 | ZERO, 783 | ZERO, 784 | (c3[0] * I - c3[2] * np.dot(CTO_1, ATO)), 785 | (c3[1] * I - c3[2] * np.dot(CTO_1, BTO)), 786 | ZERO, 787 | ZERO, 788 | ], 789 | [ 790 | ZERO, 791 | ZERO, 792 | ZERO, 793 | ZERO, 794 | ZERO, 795 | ZERO, 796 | (DTE - np.dot(np.dot(FTE, CTE_1), ATE)), 797 | (ETE - np.dot(np.dot(FTE, CTE_1), BTE)), 798 | ], 799 | ] 800 | ) 801 | ) 802 | 803 | BC_A = np.r_[BC_A1, BC_A2] 804 | 805 | x = linsolve(BC_A, BC_b) 806 | 807 | ROx, ROy, REx, REy, TOx, TOy, TEx, TEy = np.split(x, 8) 808 | 809 | ROz = -np.dot(CRO_1, (np.dot(ARO, ROx) + np.dot(BRO, ROy))) 810 | REz = -np.dot(CRE_1, (np.dot(ARE, REx) + np.dot(BRE, REy))) 811 | TOz = -np.dot(CTO_1, (np.dot(ATO, TOx) + np.dot(BTO, TOy))) 812 | TEz = -np.dot(CTE_1, (np.dot(ATE, TEx) + np.dot(BTE, TEy))) 813 | 814 | denom = (k1[2] - np.dot(u, k1) * u[2]).real 815 | DEO1[:, iwl] = ( 816 | -( 817 | (np.absolute(ROx) ** 2 + np.absolute(ROy) ** 2 + np.absolute(ROz) ** 2) 818 | * np.conj(kO1i[2, :]) 819 | - (ROx * kO1i[0, :] + ROy * kO1i[1, :] + ROz * kO1i[2, :]) 820 | * np.conj(ROz) 821 | ).real 822 | / denom 823 | ) 824 | DEE1[:, iwl] = ( 825 | -( 826 | (np.absolute(REx) ** 2 + np.absolute(REy) ** 2 + np.absolute(REz) ** 2) 827 | * np.conj(kE1i[2, :]) 828 | - (REx * kE1i[0, :] + REy * kE1i[1, :] + REz * kE1i[2, :]) 829 | * np.conj(REz) 830 | ).real 831 | / denom 832 | ) 833 | DEO3[:, iwl] = ( 834 | (np.absolute(TOx) ** 2 + np.absolute(TOy) ** 2 + np.absolute(TOz) ** 2) 835 | * np.conj(kO3i[2, :]) 836 | - (TOx * kO3i[0, :] + TOy * kO3i[1, :] + TOz * kO3i[2, :]) * np.conj(TOz) 837 | ).real / denom 838 | DEE3[:, iwl] = ( 839 | (np.absolute(TEx) ** 2 + np.absolute(TEy) ** 2 + np.absolute(TEz) ** 2) 840 | * np.conj(kE3i[2, :]) 841 | - (TEx * kE3i[0, :] + TEy * kE3i[1, :] + TEz * kE3i[2, :]) * np.conj(TEz) 842 | ).real / denom 843 | 844 | # save the results 845 | self.DEO1 = DEO1 846 | self.DEE1 = DEE1 847 | self.DEO3 = DEO3 848 | self.DEE3 = DEE3 849 | 850 | return self 851 | 852 | # def plot(self): 853 | # """Plot the diffraction efficiencies.""" 854 | # g = Gnuplot.Gnuplot() 855 | # g('set xlabel "$\lambda$"') 856 | # g('set ylabel "diffraction efficiency"') 857 | # g('set yrange [0:1]') 858 | # g('set data style linespoints') 859 | # g.plot(Gnuplot.Data(self.wls, self.DEO1[self.n,:], with_ = 'linespoints', title = 'DEO1'), \ 860 | # Gnuplot.Data(self.wls, self.DEO3[self.n,:], with_ = 'linespoints', title = 'DEO3'), \ 861 | # Gnuplot.Data(self.wls, self.DEE1[self.n,:], with_ = 'linespoints', title = 'DEE1'), \ 862 | # Gnuplot.Data(self.wls, self.DEE3[self.n,:], with_ = 'linespoints', title = 'DEE3')) 863 | # raw_input('press enter to close the graph...') 864 | 865 | def __str__(self): 866 | return ( 867 | "ANISOTROPIC RCWA SOLVER\n\n%s\n\nLAMBDA = %g\nalpha = %g\ndelta = %g\npsi = %g\nphi = %g\nn = %d" 868 | % ( 869 | self.multilayer.__str__(), 870 | self.LAMBDA, 871 | self.alpha, 872 | self.delta, 873 | self.psi, 874 | self.phi, 875 | self.n, 876 | ) 877 | ) 878 | -------------------------------------------------------------------------------- /EMpy/__init__.py: -------------------------------------------------------------------------------- 1 | """EMpy: Electromagnetic Python. 2 | 3 | The package contains some useful routines to study electromagnetic problems with Python. 4 | 5 | 1. An implementation of the L{Transfer Matrix} algorithm, both isotropic and anisotropic. 6 | 2. An implementation of the L{Rigorous Coupled Wave Analysis}, both isotropic and anisotropic. 7 | 3. A collection of L{Modesolvers} to find modes of optical waveguides. 8 | 4. A library of L{materials}. 9 | 10 | It is based on U{numpy} and U{scipy}. 11 | 12 | """ 13 | __all__ = [ 14 | "constants", 15 | "devices", 16 | "materials", 17 | "modesolvers", 18 | "RCWA", 19 | "scattering", 20 | "transfer_matrix", 21 | "utils", 22 | ] 23 | __author__ = "Lorenzo Bolla" 24 | 25 | from . import constants 26 | from . import devices 27 | from . import materials 28 | from . import modesolvers 29 | from . import RCWA 30 | from . import scattering 31 | from . import transfer_matrix 32 | from . import utils 33 | from .version import version 34 | 35 | __version__ = version 36 | -------------------------------------------------------------------------------- /EMpy/constants.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0301 2 | 3 | """Useful constants. 4 | 5 | Constants used in mathematics and electromagnetism. 6 | 7 | @var c: U{Speed of light} [m/s]. 8 | @var mu0: U{Magnetic Permeability} [N/A^2]. 9 | @var eps0: U{Electric Permettivity} [F/m]. 10 | @var h: U{Plank's constant} [W s^2]. 11 | @var k: U{Boltzmann's constant} [J/K]. 12 | 13 | """ 14 | 15 | __author__ = "Lorenzo Bolla" 16 | 17 | from numpy import pi 18 | 19 | c = 299792458.0 20 | mu0 = 4 * pi * 1e-7 21 | eps0 = 1.0 / (c**2 * mu0) 22 | 23 | h = 6.62606896e-34 24 | h_bar = h / (2 * pi) 25 | k = 1.3806504e-23 26 | -------------------------------------------------------------------------------- /EMpy/materials.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0622,R0903,R0902,R0913,W0633 2 | 3 | """Functions and objects to manipulate materials. 4 | 5 | A material is an object with a refractive index function. 6 | 7 | """ 8 | from functools import partial 9 | 10 | import numpy 11 | from scipy.integrate import quad 12 | from EMpy.constants import eps0 13 | 14 | __author__ = "Lorenzo Bolla" 15 | 16 | 17 | class Material: 18 | 19 | """Generic class to handle materials. 20 | 21 | This class is intended to be subclassed to obtain isotropic and 22 | anisotropic materials. 23 | """ 24 | 25 | def __init__(self, name=""): 26 | """Set material name.""" 27 | self.name = name 28 | 29 | 30 | class RefractiveIndex: 31 | 32 | """Refractive Index. 33 | 34 | Unaware of temperature. 35 | 36 | Parameters 37 | ---------- 38 | Provide ONE of the following named-arguments: 39 | 40 | n0_const : float 41 | A single-value of refractive index, to be used regardless of 42 | the wavelength requested. 43 | For example: 44 | >>> n0_const = 1.448 for SiO2 45 | 46 | n0_poly : list/tuple 47 | Use a polynomial rix dispersion function: provide the 48 | polynomial coefficients to be evaluated by numpy.polyval. 49 | # sets the refractive index function as n = 9(wl**3) + 50 | # 5(wl**2) + 3(wl) + 1 51 | For example: 52 | >>> n0_poly = (9,5,3,1) 53 | 54 | n0_smcoeffs (Sellmeier coefficients): 6-element list/tuple 55 | Set the rix dispersion function to the 6-parameter Sellmeier 56 | function as so: 57 | n(wls) = sqrt(1. + 58 | B1 * wls ** 2 / (wls ** 2 - C1) + 59 | B2 * wls ** 2 / (wls ** 2 - C2) + 60 | B3 * wls ** 2 / (wls ** 2 - C3)) 61 | For example: 62 | >>> n0_smcoeffs = [B1, B2, B3, C1, C2, C3] # six values total 63 | 64 | n0_func : function 65 | Provide an arbitrary function to return the refractive index 66 | versus wavelength. 67 | For example: 68 | >>> def SiN_func(wl): 69 | >>> x = wl * 1e6 # convert to microns 70 | >>> return 1.887 + 0.01929/x**2 + 1.6662e-4/x**4 # Cauchy func 71 | >>> SiN_rix = RefractiveIndex(n0_func=SiN_func) 72 | or 73 | >>> SiN_rix = RefractiveIndex( 74 | n0_func=lambda wl: 1.887 + 0.01929/(wl*1e6)**2 + 75 | 1.6662e-4/(wl*1e6)**4) 76 | 77 | Your function should return a `numpy.array`, since it will be 78 | passed a `numpy.array` of the wavelengths requested. This 79 | conversion to `array` happens automatically if your function 80 | does math on the wavelength. 81 | 82 | n0_known : dictionary 83 | Use if RefractiveIndex will only be evaluated at a specific set of `wls`. 84 | n0_known should be a dictionary of `key:value` pairs 85 | corresponding to `wavelength:rix`, for example: 86 | >>> n0_known = { 1500e-9:1.445, 1550e-9:1.446, 1600e-9:1.447 } 87 | 88 | """ 89 | 90 | def __init__( 91 | self, n0_const=None, n0_poly=None, n0_smcoeffs=None, n0_known=None, n0_func=None 92 | ): 93 | if n0_const is not None: 94 | self.get_rix = partial(self.__from_const, n0_const) 95 | 96 | elif n0_poly is not None: 97 | self.get_rix = partial(self.__from_poly, n0_poly) 98 | 99 | elif n0_smcoeffs is not None: 100 | self.get_rix = partial(self.__from_sellmeier, n0_smcoeffs) 101 | 102 | elif n0_func is not None: 103 | self.get_rix = partial(self.__from_function, n0_func) 104 | 105 | elif n0_known is not None: 106 | self.get_rix = partial(self.__from_known, n0_known) 107 | 108 | else: 109 | raise ValueError("Please provide at least one parameter.") 110 | 111 | @staticmethod 112 | def __from_const(n0, wls): 113 | wls = numpy.atleast_1d(wls) 114 | return n0 * numpy.ones_like(wls) 115 | 116 | @staticmethod 117 | def __from_poly(n0_poly, wls): 118 | wls = numpy.atleast_1d(wls) 119 | return numpy.polyval(n0_poly, wls) * numpy.ones_like(wls) 120 | 121 | @staticmethod 122 | def __from_sellmeier(n0_smcoeffs, wls): 123 | wls = numpy.atleast_1d(wls) 124 | B1, B2, B3, C1, C2, C3 = n0_smcoeffs 125 | return numpy.sqrt( 126 | 1.0 127 | + B1 * wls**2 / (wls**2 - C1) 128 | + B2 * wls**2 / (wls**2 - C2) 129 | + B3 * wls**2 / (wls**2 - C3) 130 | ) * numpy.ones_like(wls) 131 | 132 | @staticmethod 133 | def __from_function(n0_func, wls): 134 | # ensure wls is array 135 | wls = numpy.atleast_1d(wls) 136 | return n0_func(wls) * numpy.ones_like(wls) 137 | 138 | @staticmethod 139 | def __from_known(n0_known, wls): 140 | wls = numpy.atleast_1d(wls) 141 | # Note: we should interpolate... 142 | return numpy.array([n0_known.get(wl, 0) for wl in wls]) 143 | 144 | def __call__(self, wls): 145 | return self.get_rix(wls) 146 | 147 | 148 | class ThermalOpticCoefficient: 149 | 150 | """Thermal Optic Coefficient.""" 151 | 152 | def __init__(self, data=None, T0=300): 153 | self.__data = data 154 | self.T0 = T0 155 | 156 | def TOC(self, T): 157 | if self.__data is not None: 158 | return numpy.polyval(self.__data, T) 159 | else: 160 | return 0.0 161 | 162 | def __call__(self, T): 163 | return self.TOC(T) 164 | 165 | def dnT(self, T): 166 | """Integrate the TOC to get the rix variation.""" 167 | return quad(self.TOC, self.T0, T)[0] 168 | 169 | 170 | class IsotropicMaterial(Material): 171 | 172 | """Subclasses Material to describe isotropic materials. 173 | 174 | Frequency dispersion and thermic aware. 175 | In all the member functions, wls must be an ndarray. 176 | """ 177 | 178 | def __init__( 179 | self, name="", n0=RefractiveIndex(n0_const=1.0), toc=ThermalOpticCoefficient() 180 | ): 181 | """Set name, default temperature, refractive index and TOC 182 | (thermal optic coefficient).""" 183 | Material.__init__(self, name) 184 | self.n0 = n0 185 | self.toc = toc 186 | 187 | def n(self, wls, T=None): 188 | """Return the refractive index at T as a [1 x wls] array.""" 189 | if T is None: 190 | T = self.toc.T0 191 | return self.n0(wls) + self.toc.dnT(T) 192 | 193 | def epsilon(self, wls, T=None): 194 | """Return the epsilon at T as a [1 x wls] array.""" 195 | if T is None: 196 | T = self.toc.T0 197 | return self.n(wls, T) ** 2 * eps0 198 | 199 | def epsilonTensor(self, wls, T=None): 200 | """Return the epsilon at T as a [3 x 3 x wls] array.""" 201 | if T is None: 202 | T = self.toc.T0 203 | tmp = numpy.eye(3) 204 | return tmp[:, :, numpy.newaxis] * self.epsilon(wls, T) 205 | 206 | @staticmethod 207 | def isIsotropic(): 208 | """Return True, because the material is isotropic.""" 209 | return True 210 | 211 | def __str__(self): 212 | """Return material name.""" 213 | return self.name + ", isotropic" 214 | 215 | 216 | class EpsilonTensor: 217 | def __init__( 218 | self, epsilon_tensor_const=eps0 * numpy.eye(3), epsilon_tensor_known=None 219 | ): 220 | if epsilon_tensor_known is None: 221 | epsilon_tensor_known = {} 222 | self.epsilon_tensor_const = epsilon_tensor_const 223 | self.epsilon_tensor_known = epsilon_tensor_known 224 | 225 | def __call__(self, wls): 226 | """Return the epsilon tensor as a [3 x 3 x wls.size] matrix.""" 227 | wls = numpy.atleast_1d(wls) 228 | if wls.size == 1: 229 | if wls.item() in self.epsilon_tensor_known: 230 | return self.epsilon_tensor_known[wls.item()][:, :, numpy.newaxis] 231 | return self.epsilon_tensor_const[:, :, numpy.newaxis] * numpy.ones_like(wls) 232 | 233 | 234 | class AnisotropicMaterial(Material): 235 | 236 | """Subclass Material to describe anisotropic materials. 237 | 238 | No frequency dispersion nor thermic aware. 239 | In all the member functions, wls must be an ndarray. 240 | 241 | """ 242 | 243 | def __init__(self, name="", epsilon_tensor=EpsilonTensor()): 244 | """Set name and default epsilon tensor.""" 245 | Material.__init__(self, name) 246 | self.epsilonTensor = epsilon_tensor 247 | 248 | @staticmethod 249 | def isIsotropic(): 250 | """Return False, because the material is anisotropic.""" 251 | return False 252 | 253 | def __str__(self): 254 | """Return material name.""" 255 | return self.name + ", anisotropic" 256 | 257 | 258 | # Vacuum 259 | Vacuum = IsotropicMaterial(name="Vacuum") 260 | 261 | # Air 262 | Air = IsotropicMaterial(name="Air") 263 | 264 | # Silicon 265 | Si = IsotropicMaterial( 266 | name="Silicon", 267 | n0=RefractiveIndex(n0_poly=(0.076006e12, -0.31547e6, 3.783)), 268 | toc=ThermalOpticCoefficient((-1.49e-10, 3.47e-7, 9.48e-5)), 269 | ) 270 | 271 | # SiO2 272 | SiO2 = IsotropicMaterial( 273 | name="Silica", 274 | n0=RefractiveIndex(n0_const=1.446), 275 | toc=ThermalOpticCoefficient((1.1e-4,)), 276 | ) 277 | 278 | # BK7 glass (see http://en.wikipedia.org/wiki/Sellmeier_equation) 279 | BK7 = IsotropicMaterial( 280 | name="Borosilicate crown glass", 281 | n0=RefractiveIndex( 282 | n0_smcoeffs=( 283 | 1.03961212, 284 | 2.31792344e-1, 285 | 1.01046945, 286 | 6.00069867e-15, 287 | 2.00179144e-14, 288 | 1.03560653e-10, 289 | ) 290 | ), 291 | ) 292 | 293 | 294 | class LiquidCrystal(Material): 295 | 296 | """Liquid Crystal. 297 | 298 | A liquid crystal is determined by it ordinary and extraordinary 299 | refractive indices, its elastic tensor and its chirality. 300 | Inspiration here: 301 | U{http://www.ee.ucl.ac.uk/~rjames/modelling/constant-order/oned/}. 302 | 303 | @ivar name: Liquid Crystal name. 304 | @ivar nO: Ordinary refractive index. 305 | @ivar nE: Extraordinary refractive index. 306 | @ivar K11: Elastic tensor, first component. 307 | @ivar K22: Elastic tensor, second component. 308 | @ivar K33: Elastic tensor, third component. 309 | @ivar q0: Chirality. 310 | 311 | """ 312 | 313 | def __init__( 314 | self, 315 | name="", 316 | nO=1.0, 317 | nE=1.0, 318 | nO_electrical=1.0, 319 | nE_electrical=1.0, 320 | K11=0.0, 321 | K22=0.0, 322 | K33=0.0, 323 | q0=0.0, 324 | ): 325 | """Set name, the refractive indices, the elastic constants and 326 | the chirality.""" 327 | 328 | Material.__init__(self, name) 329 | self.nO = nO 330 | self.nE = nE 331 | self.nO_electrical = nO_electrical 332 | self.nE_electrical = nE_electrical 333 | self.K11 = K11 334 | self.K22 = K22 335 | self.K33 = K33 336 | self.q0 = q0 337 | self.epslow = self.nO_electrical**2 338 | self.deleps = self.nE_electrical**2 - self.epslow 339 | 340 | 341 | def get_10400_000_100(conc000): 342 | """Return a LiquidCrystal made of conc% 000 and (100-conc)% 100.""" 343 | 344 | conc = [0, 100] 345 | epsO_electrical = [3.38, 3.28] 346 | epsE_electrical = [5.567, 5.867] 347 | epsO = [1.47551**2, 1.46922**2] 348 | epsE = [1.61300**2, 1.57016**2] 349 | 350 | K11 = 13.5e-12 # elastic constant [N] (splay) 351 | K22 = 6.5e-12 # elastic constant [N] (twist) 352 | K33 = 20e-12 # elastic constant [N] (bend) 353 | q0 = 0 # chirality 2*pi/pitch 354 | 355 | nO_electrical_ = numpy.interp(conc000, conc, epsO_electrical) ** 0.5 356 | nE_electrical_ = numpy.interp(conc000, conc, epsE_electrical) ** 0.5 357 | nO_ = numpy.interp(conc000, conc, epsO) ** 0.5 358 | nE_ = numpy.interp(conc000, conc, epsE) ** 0.5 359 | 360 | return LiquidCrystal( 361 | "10400_000_100_" + str(conc000) + "_" + str(100 - conc000), 362 | nO_, 363 | nE_, 364 | nO_electrical_, 365 | nE_electrical_, 366 | K11, 367 | K22, 368 | K33, 369 | q0, 370 | ) 371 | -------------------------------------------------------------------------------- /EMpy/modesolvers/__init__.py: -------------------------------------------------------------------------------- 1 | """Collection of different algorithms to find modes of optical waveguides. 2 | 3 | Modesolvers: 4 | 5 | 1. FD. 6 | 2. FMM. 7 | 8 | 9 | """ 10 | __all__ = ["FD", "FMM", "geometries"] 11 | __author__ = "Lorenzo Bolla" 12 | 13 | from . import FD, FMM, geometries 14 | -------------------------------------------------------------------------------- /EMpy/modesolvers/geometries.py: -------------------------------------------------------------------------------- 1 | """Geometries. 2 | 3 | """ 4 | import numpy 5 | import pylab 6 | 7 | from EMpy.modesolvers.interface import interface_matrix 8 | 9 | 10 | def S2T(S): 11 | dim = S.shape[0] / 2.0 12 | s11 = S[:dim, :dim] 13 | s22 = S[dim:, dim:] 14 | s12 = S[:dim, dim:] 15 | s21 = S[dim:, :dim] 16 | T = numpy.zeros_like(S) 17 | s12_1 = numpy.linalg.inv(s12) 18 | T[:dim, :dim] = s21 - numpy.dot(numpy.dot(s22, s12_1), s11) 19 | T[dim:, dim:] = s12_1 20 | T[:dim, dim:] = numpy.dot(s22, s12_1) 21 | T[dim:, :dim] = -numpy.dot(s12_1, s11) 22 | return T 23 | 24 | 25 | def T2S(T): 26 | dim = T.shape[0] / 2.0 27 | t11 = T[:dim, :dim] 28 | t22 = T[dim:, dim:] 29 | t12 = T[:dim, dim:] 30 | t21 = T[dim:, :dim] 31 | S = numpy.zeros_like(T) 32 | t22_1 = numpy.linalg.inv(t22) 33 | S[:dim, :dim] = -numpy.dot(t22_1, t21) 34 | S[dim:, dim:] = numpy.dot(t12, t22_1) 35 | S[:dim, dim:] = t22_1 36 | S[dim:, :dim] = t11 - numpy.dot(numpy.dot(t12, t22_1), t21) 37 | return S 38 | 39 | 40 | class SWG: 41 | # def __init__(self, cs, solver, length, inputLHS=None, inputRHS=None): 42 | def __init__(self, solver, length, inputLHS=None, inputRHS=None): 43 | # self.cs = cs 44 | self.solver = solver 45 | self.length = length 46 | self.build_matrix() 47 | self.compute_output(inputLHS, inputRHS) 48 | 49 | def compute_output(self, inputLHS=None, inputRHS=None): 50 | if inputLHS is None: 51 | self.inputLHS = numpy.zeros(self.solver.nmodes) 52 | self.inputLHS[0] = 1.0 53 | else: 54 | self.inputLHS = inputLHS 55 | if inputRHS is None: 56 | self.inputRHS = numpy.zeros(self.solver.nmodes) 57 | else: 58 | self.inputRHS = inputRHS 59 | input = numpy.r_[self.inputLHS, self.inputRHS] 60 | output = numpy.dot(self.S, input) 61 | self.outputLHS = output[: self.solver.nmodes] 62 | self.outputRHS = output[self.solver.nmodes :] 63 | 64 | def build_matrix(self): 65 | neffs = numpy.array([m.neff for m in self.solver.modes]) 66 | betas = 2 * numpy.pi / self.solver.wl * neffs 67 | neigs = self.solver.nmodes 68 | T = numpy.zeros((2 * neigs, 2 * neigs), dtype=complex) 69 | T[:neigs, :neigs] = numpy.diag(numpy.exp(1j * betas * self.length)) 70 | T[neigs:, neigs:] = numpy.diag(numpy.exp(-1j * numpy.conj(betas) * self.length)) 71 | self.T = T 72 | self.S = T2S(self.T) 73 | 74 | def plot(self, sumx=1, nxy=100, nz=100, z0=0): 75 | if sumx is None: # sum in y 76 | x = self.solver.modes[0].get_y(nxy) 77 | axis = 0 78 | else: # sum in x 79 | x = self.solver.modes[0].get_x(nxy) 80 | axis = 1 81 | z = numpy.linspace(0, self.length, nz) 82 | const_z = numpy.ones_like(z) 83 | f = numpy.zeros((len(x), len(z)), dtype=complex) 84 | for im, (coeffLHS, coeffRHS) in enumerate(zip(self.inputLHS, self.inputRHS)): 85 | m = self.solver.modes[im] 86 | # beta = 2 * numpy.pi / self.solver.wl * m.neff 87 | tmp = numpy.sum(m.intensity(x, x), axis=axis) 88 | f += ( 89 | numpy.abs(coeffLHS) ** 2 * tmp[:, numpy.newaxis] * const_z 90 | + numpy.abs(coeffRHS) ** 2 * tmp[:, numpy.newaxis] * const_z 91 | ) 92 | pylab.hot() 93 | pylab.contourf(x, z0 + z, numpy.abs(f).T, 16) 94 | 95 | 96 | class SimpleJoint: 97 | # def __init__(self, cs1, cs2, solver1, solver2, inputLHS=None, inputRHS=None): 98 | def __init__(self, solver1, solver2, inputLHS=None, inputRHS=None): 99 | # self.cs1 = cs1 100 | self.solver1 = solver1 101 | # self.cs2 = cs2 102 | self.solver2 = solver2 103 | self.length = 0.0 104 | self.build_matrix() 105 | self.compute_output(inputLHS, inputRHS) 106 | 107 | def compute_output(self, inputLHS=None, inputRHS=None): 108 | if inputLHS is None: 109 | self.inputLHS = numpy.zeros(self.solver1.nmodes) 110 | self.inputLHS[0] = 1.0 111 | else: 112 | self.inputLHS = inputLHS 113 | if inputRHS is None: 114 | self.inputRHS = numpy.zeros(self.solver1.nmodes) 115 | else: 116 | self.inputRHS = inputRHS 117 | input = numpy.r_[self.inputLHS, self.inputRHS] 118 | output = numpy.dot(self.S, input) 119 | self.outputLHS = output[: self.solver1.nmodes] 120 | self.outputRHS = output[self.solver1.nmodes :] 121 | 122 | def build_matrix(self): 123 | O11, O22, O12, O21 = interface_matrix(self.solver1, self.solver2) 124 | neffA = numpy.array([m.neff for m in self.solver1.modes]) 125 | neffB = numpy.array([m.neff for m in self.solver2.modes]) 126 | betaA = 2 * numpy.pi / self.solver1.wl * neffA 127 | betaB = 2 * numpy.pi / self.solver1.wl * neffB 128 | 129 | BetaA = numpy.diag(betaA) 130 | BetaB = numpy.diag(betaB) 131 | 132 | # MRA1 = numpy.dot(O12, BetaA + BetaB) 133 | # MRA2 = numpy.dot(O12, BetaA - BetaB) 134 | # MRA = numpy.dot(numpy.linalg.inv(MRA1), MRA2) 135 | # S11 = MRA 136 | # 137 | # MRB1 = numpy.dot(O21, BetaA + BetaB) 138 | # MRB2 = numpy.dot(O21, BetaB - BetaA) 139 | # MRB = numpy.dot(numpy.linalg.inv(MRB1), MRB2) 140 | # S22 = MRB 141 | # 142 | # MTAB1 = numpy.dot(O22, BetaA + BetaB) 143 | # MTAB2 = numpy.dot(O12, 2. * BetaA) 144 | # MTAB = numpy.dot(numpy.linalg.inv(MTAB1), MTAB2) 145 | # S12 = MTAB 146 | # 147 | # MTBA1 = numpy.dot(O11, BetaA + BetaB) 148 | # MTBA2 = numpy.dot(O21, 2. * BetaB) 149 | # MTBA = numpy.dot(numpy.linalg.inv(MTBA1), MTBA2) 150 | # S21 = MTBA 151 | 152 | SUM = numpy.dot(BetaA, O21) + numpy.dot(O21, BetaB) 153 | DIF = numpy.dot(BetaA, O21) - numpy.dot(O21, BetaB) 154 | A = 0.5 * numpy.dot(numpy.linalg.inv(BetaA), SUM) 155 | B = 0.5 * numpy.dot(numpy.linalg.inv(BetaA), DIF) 156 | A_1 = numpy.linalg.inv(A) 157 | S11 = numpy.dot(B, A_1) 158 | S12 = A - numpy.dot(numpy.dot(B, A_1), B) 159 | S21 = A_1 160 | S22 = -numpy.dot(A_1, B) 161 | 162 | dim = O11.shape[0] 163 | 164 | S = numpy.zeros((2 * dim, 2 * dim), dtype=complex) 165 | S[:dim, :dim] = S11 166 | S[dim:, dim:] = S22 167 | S[:dim, dim:] = S12 168 | S[dim:, :dim] = S21 169 | 170 | self.S = S 171 | self.T = S2T(self.S) 172 | 173 | def plot(self, sumx=1, nxy=100, nz=100, z0=0): 174 | pass 175 | 176 | 177 | class GenericDevice: 178 | def __init__(self, devlist, inputLHS=None, inputRHS=None): 179 | self.devlist = devlist 180 | self.build_matrix() 181 | self.compute_output(inputLHS, inputRHS) 182 | 183 | def compute_output(self, inputLHS=None, inputRHS=None): 184 | if inputLHS is None: 185 | self.inputLHS = numpy.zeros_like(self.devlist[0].inputLHS) 186 | self.inputLHS[0] = 1.0 187 | else: 188 | self.inputLHS = inputLHS 189 | if inputRHS is None: 190 | self.inputRHS = numpy.zeros_like(self.devlist[0].inputRHS) 191 | else: 192 | self.inputRHS = inputRHS 193 | input = numpy.r_[self.inputLHS, self.inputRHS] 194 | output = numpy.dot(self.S, input) 195 | self.outputLHS = output[: len(self.inputLHS)] 196 | self.outputRHS = output[len(self.inputLHS) :] 197 | # compute for each device 198 | inputLHS = self.inputLHS 199 | outputLHS = self.outputLHS 200 | dim = len(inputLHS) 201 | for d in self.devlist: 202 | d.inputLHS = inputLHS 203 | d.outputLHS = outputLHS 204 | LHS = numpy.r_[inputLHS, outputLHS] 205 | RHS = numpy.dot(d.T, LHS) 206 | d.outputRHS = RHS[:dim] 207 | d.inputRHS = RHS[dim:] 208 | inputLHS = d.outputRHS 209 | outputLHS = d.inputRHS 210 | 211 | def build_matrix(self): 212 | dim = self.devlist[0].solver.nmodes 213 | T = numpy.eye(2 * dim, dtype=complex) 214 | for d in self.devlist: 215 | T = numpy.dot(d.T, T) 216 | self.T = T 217 | self.S = T2S(self.T) 218 | 219 | def plot(self, sumx=1, nxy=100, nz=100, z0=0): 220 | z = z0 221 | for d in self.devlist: 222 | d.plot(sumx, nxy, nz, z0=z) 223 | z += d.length 224 | pylab.ylabel("z") 225 | if sumx is None: 226 | pylab.xlabel("y") 227 | else: 228 | pylab.xlabel("x") 229 | pylab.axis("tight") 230 | -------------------------------------------------------------------------------- /EMpy/modesolvers/interface.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | 4 | class ModeSolver: 5 | def solve(self, *argv): 6 | raise NotImplementedError() 7 | 8 | 9 | class Mode: 10 | def get_x(self, x=None, y=None): 11 | raise NotImplementedError() 12 | 13 | def get_y(self, x=None, y=None): 14 | raise NotImplementedError() 15 | 16 | def intensity(self, x=None, y=None): 17 | raise NotImplementedError() 18 | 19 | def TEfrac(self, x=None, y=None): 20 | raise NotImplementedError() 21 | 22 | def overlap(self, x=None, y=None): 23 | raise NotImplementedError() 24 | 25 | def get_fields_for_FDTD(self, x, y): 26 | raise NotImplementedError() 27 | 28 | def save_for_FDTD(self, mode_id="", x=None, y=None): 29 | """Save mode's fields on a staggered grid.""" 30 | Ex_FDTD, Ey_FDTD, Ez_FDTD, Hx_FDTD, Hy_FDTD, Hz_FDTD = self.get_fields_for_FDTD( 31 | x, y 32 | ) 33 | Ex_FDTD.real.T.tofile("ex" + mode_id + ".dat", sep=" ") 34 | Ex_FDTD.imag.T.tofile("iex" + mode_id + ".dat", sep=" ") 35 | Ey_FDTD.real.T.tofile("ey" + mode_id + ".dat", sep=" ") 36 | Ey_FDTD.imag.T.tofile("iey" + mode_id + ".dat", sep=" ") 37 | Ez_FDTD.real.T.tofile("ez" + mode_id + ".dat", sep=" ") 38 | Ez_FDTD.imag.T.tofile("iez" + mode_id + ".dat", sep=" ") 39 | Hx_FDTD.real.T.tofile("hx" + mode_id + ".dat", sep=" ") 40 | Hx_FDTD.imag.T.tofile("ihx" + mode_id + ".dat", sep=" ") 41 | Hy_FDTD.real.T.tofile("hy" + mode_id + ".dat", sep=" ") 42 | Hy_FDTD.imag.T.tofile("ihy" + mode_id + ".dat", sep=" ") 43 | Hz_FDTD.real.T.tofile("hz" + mode_id + ".dat", sep=" ") 44 | Hz_FDTD.imag.T.tofile("ihz" + mode_id + ".dat", sep=" ") 45 | 46 | def plot(self, x=None, y=None): 47 | raise NotImplementedError() 48 | 49 | 50 | def overlap(m1, m2, x=None, y=None): 51 | return m1.overlap(m2, x, y) 52 | 53 | 54 | def interface_matrix(solver1, solver2, x=None, y=None): 55 | neigs = solver1.nmodes 56 | 57 | O11 = numpy.zeros((neigs, neigs), dtype=complex) 58 | O22 = numpy.zeros((neigs, neigs), dtype=complex) 59 | O12 = numpy.zeros((neigs, neigs), dtype=complex) 60 | O21 = numpy.zeros((neigs, neigs), dtype=complex) 61 | 62 | for i in range(neigs): 63 | for j in range(neigs): 64 | O11[i, j] = overlap(solver1.modes[i], solver1.modes[j], x, y) 65 | O22[i, j] = overlap(solver2.modes[i], solver2.modes[j], x, y) 66 | O12[i, j] = overlap(solver1.modes[i], solver2.modes[j], x, y) 67 | O21[i, j] = overlap(solver2.modes[i], solver1.modes[j], x, y) 68 | 69 | return (O11, O22, O12, O21) 70 | -------------------------------------------------------------------------------- /EMpy/scattering.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import EMpy 3 | 4 | __author__ = "Julien Hillairet" 5 | 6 | 7 | class Field: 8 | """Class to describe an electromagnetic field. 9 | 10 | A EM field is a vector field which is the combination of an electric field E and a magnetic field H. 11 | These fields are defined on some space point r 12 | 13 | @ivar E: Electric vector on point M{r} in the cartesian coordinate system M{(Ex,Ey,Ez)} 14 | @type E: 2d numpy.ndarray of shape (3,N) 15 | @ivar H: Magnetic vector on point M{r} in the cartesian coordinate system M{(Mx,My,Mz)} 16 | @type H: 2d numpy.ndarray of shape (3,N) 17 | @ivar r: Position vector in cartesian coordinates M{(x,y,z)} 18 | @type r: 2d numpy.ndarray of shape (3,N) 19 | 20 | """ 21 | 22 | def __init__( 23 | self, E=numpy.zeros((3, 1)), H=numpy.zeros((3, 1)), r=numpy.zeros((3, 1)) 24 | ): 25 | """Initialize the Field object with an Electric, Magnetic and Position vectors.""" 26 | self.E = E 27 | self.H = H 28 | self.r = r 29 | 30 | 31 | """ 32 | ############################################################## 33 | utils functions for field manipulations 34 | ############################################################## 35 | """ 36 | 37 | 38 | def stack(X, Y, Z): 39 | """Stack 3 vectors of different lengths into one vector. 40 | 41 | stack 3 vectors of different length to one vector of type 42 | M{P(n) = [x(n); y(n); z(n)]} with M{n=[1:Nx*Ny*Nz]} 43 | 44 | Examples 45 | ======== 46 | >>> import numpy 47 | >>> X = numpy.arange(4); Y = numpy.arange(3); Z = numpy.arange(2) 48 | >>> S = field.stack(X,Y,Z) 49 | >>> print S 50 | [[ 0. 0. 0. 0. 0. 0. 1. 1. 1. 1. 1. 1. 2. 2. 2. 2. 2. 2. 3. 3. 3. 3. 3. 3.] 51 | [ 0. 0. 1. 1. 2. 2. 0. 0. 1. 1. 2. 2. 0. 0. 1. 1. 2. 2. 0. 0. 1. 1. 2. 2.] 52 | [ 0. 1. 0. 1. 0. 1. 0. 1. 0. 1. 0. 1. 0. 1. 0. 1. 0. 1. 0. 1. 0. 1. 0. 1.]] 53 | 54 | Checking the size of the returned array 55 | >>> numpy.size(S,axis=1) == numpy.size(X,axis=0)*numpy.size(Y,axis=0)*numpy.size(Z,axis=0) 56 | True 57 | 58 | @param X: first vector.of length Nx 59 | @type X: numpy.ndarray of shape (1, Nx). 60 | @param Y: second vector.of length Ny 61 | @type Y: numpy.ndarray of shape (1, Ny). 62 | @param Z: third vector.of length Nz 63 | @type Z: numpy.ndarray of shape (1, Nz). 64 | 65 | @return: stacked array. 66 | @rtype: numpy.ndarray of shape (3, Nx*Ny*Nz) 67 | 68 | """ 69 | Nx = numpy.size(X, axis=0) 70 | Ny = numpy.size(Y, axis=0) 71 | Nz = numpy.size(Z, axis=0) 72 | 73 | XX = numpy.reshape(numpy.transpose(numpy.ones((Ny * Nz, 1)) * X), (1, Ny * Nz * Nx)) 74 | YY = numpy.tile( 75 | numpy.reshape(numpy.transpose(numpy.ones((Nz, 1)) * Y), (1, Ny * Nz)), (1, Nx) 76 | ) 77 | ZZ = numpy.tile(Z, (1, Nx * Ny)) 78 | 79 | return numpy.vstack((XX, YY, ZZ)) 80 | 81 | 82 | def matlab_dot(a, b): 83 | """ 84 | "classical" dot product (as defined in octave or matlab) 85 | 86 | Example 87 | ======= 88 | >>> a=numpy.array([1,2,3]).repeat(6).reshape((3,6)) 89 | >>> print a 90 | [[1 1 1 1 1 1] 91 | [2 2 2 2 2 2] 92 | [3 3 3 3 3 3]] 93 | >>> field.matlab_dot(a,a) 94 | array([14, 14, 14, 14, 14, 14]) 95 | 96 | where M{14=1*1+2*2+3*3} 97 | 98 | @param a: first vector of length N 99 | @type a: numpy.ndarray of shape (M,N) 100 | @param b: second vector of length N 101 | @type b: numpy.ndarray of shape (M,N) 102 | 103 | @return: "classical" dot product between M{a} and M{b} 104 | @rtype: numpy.ndarray of shape (1,N) 105 | 106 | """ 107 | return numpy.sum(a * b, axis=0) 108 | 109 | 110 | """ 111 | ########################################## 112 | Common functions for calculating EM fields 113 | ########################################## 114 | """ 115 | 116 | 117 | def currentsScatteringKottler(P, J, M, Q, dS, f, epsr=1): 118 | """ 119 | Compute the scattered fields on some point M{P} in cartesian coordinates M{P(Px,Py,Pz)} 120 | by electric (and even magnetic) currents densities M{J}, M{M} 121 | defined on a surface M{Q(Qx,Qy,Qz)}. 122 | M{dS} corresponds to the surface element M{dS} of surface M{Q} 123 | 124 | TODO: 125 | - Maybe an option to compute only main expression in farfield 126 | - Optimisation ? (At the present time, python version is slower than the matlab code) 127 | 128 | @param P: observation points M{P(Px,Py,Pz)}. 129 | @type P: numpy.ndarray of shape (3,NbP) 130 | @param J: electric current density on M{Q} 131 | @type J: numpy.ndarray of shape (3,NbQ) 132 | @param M: magnetic current density on M{Q} 133 | @type M: numpy.ndarray of shape (3,NbQ) 134 | @param Q: radiating (surface) points M{Q(Qx,Qy,Qz)}. 135 | @type Q: numpy.ndarray of shape (3,NbQ) 136 | @param dS: surface elements of the radiating surface 137 | @type dS: numpy.ndarray of shape (1,NbQ) 138 | @param f: frequency 139 | @type f: float 140 | @param epsr: relative permittivity (dielectric constant) (defaut : 1, vacuum) 141 | @type epsr: float 142 | 143 | @return: EM Field on M{P} 144 | @rtype: L{Field} 145 | 146 | 147 | """ 148 | # test if the parameters size are correct 149 | if ( 150 | numpy.size(P, axis=0) != 3 151 | or numpy.size(Q, axis=0) != 3 152 | or numpy.size(J, axis=0) != 3 153 | or numpy.size(M, axis=0) != 3 154 | ): 155 | EMpy.utils.warning( 156 | "Bad parameters size : number of rows must be 3 for vectors P,Q,J,M" 157 | ) 158 | 159 | if ( 160 | not numpy.size(Q, axis=1) 161 | == numpy.size(J, axis=1) 162 | == numpy.size(M, axis=1) 163 | == numpy.size(dS, axis=1) 164 | ): 165 | EMpy.utils.warning( 166 | "Bad parameters size : number of columns between Q,J,M and dS must be equal" 167 | ) 168 | 169 | lambda0 = EMpy.constants.c / f 170 | lambdam = lambda0 / numpy.sqrt(epsr) 171 | # k0 = 2*numpy.pi/lambda0 172 | km = 2 * numpy.pi / lambdam 173 | Z0 = 120 * numpy.pi 174 | NbP = numpy.size(P, axis=1) 175 | NbQ = numpy.size(Q, axis=1) 176 | 177 | # preallocation 178 | EMF_P = Field( 179 | numpy.zeros((3, NbP), dtype=complex), numpy.zeros((3, NbP), dtype=complex), P 180 | ) 181 | 182 | # for all observation point 183 | # PB = EMpy.utils.ProgressBar() 184 | for ind in numpy.arange(NbP): 185 | # distance between scattering and observation point 186 | QP = P[:, ind].reshape((3, 1)) * numpy.ones((1, NbQ)) - Q 187 | r = numpy.sqrt(numpy.sum(QP**2, axis=0)) 188 | # unit vector 189 | r1 = QP / r 190 | # integrand expression shorcuts 191 | kmr = km * r 192 | kmr_1 = 1 / kmr 193 | kmr_2 = kmr_1**2 194 | aa = 1 - 1j * kmr_1 - kmr_2 195 | bb = -1 + 3 * 1j * kmr_1 + 3 * kmr_2 196 | cc = 1 - 1j * kmr_1 197 | # scalar product 198 | r1dotJ = matlab_dot(r1, J) 199 | r1dotM = matlab_dot(r1, -M) 200 | # Kottler EM fields 201 | EMF_P.E[:, ind] = ( 202 | km 203 | / (4 * numpy.pi * 1j) 204 | * numpy.sum( 205 | ( 206 | Z0 207 | / numpy.sqrt(epsr) 208 | * (aa * J + bb * (r1dotJ * numpy.ones((3, 1))) * r1) 209 | + cc * numpy.cross(r1, -M, axis=0) 210 | ) 211 | * numpy.exp(-1j * km * r) 212 | / r 213 | * dS, 214 | axis=1, 215 | ) 216 | ) 217 | EMF_P.H[:, ind] = ( 218 | -km 219 | / (4 * numpy.pi * 1j) 220 | * numpy.sum( 221 | ( 222 | numpy.sqrt(epsr) 223 | / Z0 224 | * (-aa * M + bb * (r1dotM * numpy.ones((3, 1))) * r1) 225 | - cc * numpy.cross(r1, J, axis=0) 226 | ) 227 | * numpy.exp(-1j * km * r) 228 | / r 229 | * dS, 230 | axis=1, 231 | ) 232 | ) 233 | # waitbar update 234 | # PB.update((ind+1)*100.0/NbP) 235 | 236 | return EMF_P 237 | -------------------------------------------------------------------------------- /EMpy/transfer_matrix.py: -------------------------------------------------------------------------------- 1 | """Transfer matrix for isotropic and anisotropic multilayer. 2 | 3 | The transfer matrix algorithms is used to compute the power reflected 4 | and transmitted by a multilayer. 5 | The multilayer can be made of isotropic and anisotropic layers, with 6 | any thickness. 7 | Two versions of the algorithm are present: an isotropic one and an 8 | anisotropic one. 9 | """ 10 | __author__ = "Lorenzo Bolla" 11 | 12 | import numpy as np 13 | from scipy.linalg import inv 14 | from EMpy.utils import snell, norm 15 | from EMpy.constants import c, mu0 16 | 17 | # import Gnuplot 18 | 19 | 20 | class TransferMatrix: 21 | 22 | """Class to handle the transfer matrix solvers.""" 23 | 24 | def __init__(self, multilayer): 25 | """Set the multilayer. 26 | 27 | INPUT 28 | multilayer = Multilayer obj describing the sequence of layers. 29 | """ 30 | self.setMultilayer(multilayer) 31 | 32 | def setMultilayer(self, m): 33 | self.multilayer = m.simplify() 34 | 35 | 36 | class IsotropicTransferMatrix(TransferMatrix): 37 | def __init__(self, multilayer, theta_inc): 38 | """Set the multilayer and the incident angle. 39 | 40 | INPUT 41 | multilayer = Multilayer obj describing the sequence of layers. 42 | theta_inc = angle of the incident wave (in radiant) wrt the 43 | normal. 44 | """ 45 | if not multilayer.isIsotropic(): 46 | raise ValueError( 47 | "Cannot use IsotropicTransferMatrix with anisotropic multilayer" 48 | ) 49 | TransferMatrix.__init__(self, multilayer) 50 | self.theta_inc = theta_inc 51 | 52 | def solve(self, wls): 53 | """Isotropic solver. 54 | 55 | INPUT 56 | wls = wavelengths to scan (any asarray-able object). 57 | 58 | OUTPUT 59 | self.Rs, self.Ts, self.Rp, self.Tp = power reflected and 60 | transmitted on s and p polarizations. 61 | """ 62 | 63 | self.wls = np.asarray(wls) 64 | 65 | multilayer = self.multilayer 66 | theta_inc = self.theta_inc 67 | 68 | nlayers = len(multilayer) 69 | d = np.array([l.thickness for l in multilayer]).ravel() 70 | 71 | Rs = np.zeros_like(self.wls, dtype=float) 72 | Ts = np.zeros_like(self.wls, dtype=float) 73 | Rp = np.zeros_like(self.wls, dtype=float) 74 | Tp = np.zeros_like(self.wls, dtype=float) 75 | 76 | Dp = np.zeros((2, 2), dtype=complex) 77 | Ds = np.zeros((2, 2), dtype=complex) 78 | P = np.zeros((2, 2), dtype=complex) 79 | Ms = np.zeros((2, 2), dtype=complex) 80 | Mp = np.zeros((2, 2), dtype=complex) 81 | k = np.zeros((nlayers, 2), dtype=complex) 82 | 83 | ntot = np.zeros((self.wls.size, nlayers), dtype=complex) 84 | for i, l in enumerate(multilayer): 85 | # ntot[:,i] = l.mat.n(self.wls,l.mat.T0) 86 | ntot[:, i] = l.mat.n(self.wls, l.mat.toc.T0) 87 | 88 | for iwl, wl in enumerate(self.wls): 89 | n = ntot[iwl, :] 90 | theta = snell(theta_inc, n) 91 | 92 | k[:, 0] = 2 * np.pi * n / wl * np.cos(theta) 93 | k[:, 1] = 2 * np.pi * n / wl * np.sin(theta) 94 | 95 | Ds = [[1.0, 1.0], [n[0] * np.cos(theta[0]), -n[0] * np.cos(theta[0])]] 96 | Dp = [[np.cos(theta[0]), np.cos(theta[0])], [n[0], -n[0]]] 97 | Ms = inv(Ds) 98 | Mp = inv(Dp) 99 | 100 | for nn, dd, tt, kk in zip(n[1:-1], d[1:-1], theta[1:-1], k[1:-1, 0]): 101 | Ds = [[1.0, 1.0], [nn * np.cos(tt), -nn * np.cos(tt)]] 102 | Dp = [[np.cos(tt), np.cos(tt)], [nn, -nn]] 103 | phi = kk * dd 104 | P = [[np.exp(1j * phi), 0], [0, np.exp(-1j * phi)]] 105 | Ms = np.dot(Ms, np.dot(Ds, np.dot(P, inv(Ds)))) 106 | Mp = np.dot(Mp, np.dot(Dp, np.dot(P, inv(Dp)))) 107 | 108 | Ds = [[1.0, 1.0], [n[-1] * np.cos(theta[-1]), -n[-1] * np.cos(theta[-1])]] 109 | Dp = [[np.cos(theta[-1]), np.cos(theta[-1])], [n[-1], -n[-1]]] 110 | Ms = np.dot(Ms, Ds) 111 | Mp = np.dot(Mp, Dp) 112 | 113 | rs = Ms[1, 0] / Ms[0, 0] 114 | ts = 1.0 / Ms[0, 0] 115 | 116 | rp = Mp[1, 0] / Mp[0, 0] 117 | tp = 1.0 / Mp[0, 0] 118 | 119 | Rs[iwl] = np.absolute(rs) ** 2 120 | Ts[iwl] = ( 121 | np.absolute((n[-1] * np.cos(theta[-1])) / (n[0] * np.cos(theta[0]))) 122 | * np.absolute(ts) ** 2 123 | ) 124 | Rp[iwl] = np.absolute(rp) ** 2 125 | Tp[iwl] = ( 126 | np.absolute((n[-1] * np.cos(theta[-1])) / (n[0] * np.cos(theta[0]))) 127 | * np.absolute(tp) ** 2 128 | ) 129 | 130 | self.Rs = Rs 131 | self.Ts = Ts 132 | self.Rp = Rp 133 | self.Tp = Tp 134 | return self 135 | 136 | # def plot(self): 137 | # """Plot the solution.""" 138 | # g = Gnuplot.Gnuplot() 139 | # g('set xlabel "$\lambda$"') 140 | # g('set ylabel "power"') 141 | # g('set yrange [0:1]') 142 | # g('set data style linespoints') 143 | # g.plot(Gnuplot.Data(self.wls, self.Rs, with_ = 'linespoints', title = 'Rs'), \ 144 | # Gnuplot.Data(self.wls, self.Ts, with_ = 'linespoints', title = 'Ts'), \ 145 | # Gnuplot.Data(self.wls, self.Rp, with_ = 'linespoints', title = 'Rp'), \ 146 | # Gnuplot.Data(self.wls, self.Tp, with_ = 'linespoints', title = 'Tp')) 147 | # raw_input('press enter to close the graph...') 148 | 149 | def __str__(self): 150 | return "ISOTROPIC TRANSFER MATRIX SOLVER\n\n%s\n\ntheta inc = %g" % ( 151 | self.multilayer.__str__(), 152 | self.theta_inc, 153 | ) 154 | 155 | 156 | class AnisotropicTransferMatrix(TransferMatrix): 157 | def __init__(self, multilayer, theta_inc_x, theta_inc_y): 158 | """Set the multilayer and the incident angle. 159 | 160 | INPUT 161 | multilayer = Multilayer obj describing the sequence of layers. 162 | theta_inc_x and theta_inc_y = angles of the incident wave (in 163 | radiant) wrt the normal. 164 | """ 165 | TransferMatrix.__init__(self, multilayer) 166 | self.theta_inc_x = theta_inc_x 167 | self.theta_inc_y = theta_inc_y 168 | 169 | def solve(self, wls): 170 | """Anisotropic solver. 171 | 172 | INPUT 173 | wls = wavelengths to scan (any asarray-able object). 174 | 175 | OUTPUT 176 | self.R, self.T = power reflected and transmitted. 177 | """ 178 | 179 | self.wls = np.asarray(wls) 180 | 181 | multilayer = self.multilayer 182 | theta_inc_x = self.theta_inc_x 183 | theta_inc_y = self.theta_inc_y 184 | 185 | def find_roots(wl, epsilon, alpha, beta): 186 | """Find roots of characteristic equation. 187 | 188 | Given a wavelength, a 3x3 tensor epsilon and the tangential components 189 | of the wavevector k = (alpha,beta,gamma_i), returns the 4 possible 190 | gamma_i, i = 1,2,3,4 that satisfy the boundary conditions. 191 | """ 192 | 193 | omega = 2.0 * np.pi * c / wl 194 | K = omega**2 * mu0 * epsilon 195 | 196 | k0 = 2.0 * np.pi / wl 197 | K /= k0**2 198 | alpha /= k0 199 | beta /= k0 200 | 201 | alpha2 = alpha**2 202 | alpha3 = alpha**3 203 | alpha4 = alpha**4 204 | beta2 = beta**2 205 | beta3 = beta**3 206 | beta4 = beta**4 207 | 208 | coeff = [ 209 | K[2, 2], 210 | alpha * (K[0, 2] + K[2, 0]) + beta * (K[1, 2] + K[2, 1]), 211 | alpha2 * (K[0, 0] + K[2, 2]) 212 | + alpha * beta * (K[1, 0] + K[0, 1]) 213 | + beta2 * (K[1, 1] + K[2, 2]) 214 | + ( 215 | K[0, 2] * K[2, 0] 216 | + K[1, 2] * K[2, 1] 217 | - K[0, 0] * K[2, 2] 218 | - K[1, 1] * K[2, 2] 219 | ), 220 | alpha3 * (K[0, 2] + K[2, 0]) 221 | + beta3 * (K[1, 2] + K[2, 1]) 222 | + alpha2 * beta * (K[1, 2] + K[2, 1]) 223 | + alpha * beta2 * (K[0, 2] + K[2, 0]) 224 | + alpha 225 | * ( 226 | K[0, 1] * K[1, 2] 227 | + K[1, 0] * K[2, 1] 228 | - K[0, 2] * K[1, 1] 229 | - K[2, 0] * K[1, 1] 230 | ) 231 | + beta 232 | * ( 233 | K[0, 1] * K[2, 0] 234 | + K[1, 0] * K[0, 2] 235 | - K[0, 0] * K[1, 2] 236 | - K[0, 0] * K[2, 1] 237 | ), 238 | alpha4 * (K[0, 0]) 239 | + beta4 * (K[1, 1]) 240 | + alpha3 * beta * (K[0, 1] + K[1, 0]) 241 | + alpha * beta3 * (K[0, 1] + K[1, 0]) 242 | + alpha2 * beta2 * (K[0, 0] + K[1, 1]) 243 | + alpha2 244 | * ( 245 | K[0, 1] * K[1, 0] 246 | + K[0, 2] * K[2, 0] 247 | - K[0, 0] * K[2, 2] 248 | - K[0, 0] * K[1, 1] 249 | ) 250 | + beta2 251 | * ( 252 | K[0, 1] * K[1, 0] 253 | + K[1, 2] * K[2, 1] 254 | - K[0, 0] * K[1, 1] 255 | - K[1, 1] * K[2, 2] 256 | ) 257 | + alpha 258 | * beta 259 | * ( 260 | K[0, 2] * K[2, 1] 261 | + K[2, 0] * K[1, 2] 262 | - K[0, 1] * K[2, 2] 263 | - K[1, 0] * K[2, 2] 264 | ) 265 | + K[0, 0] * K[1, 1] * K[2, 2] 266 | - K[0, 0] * K[1, 2] * K[2, 1] 267 | - K[1, 0] * K[0, 1] * K[2, 2] 268 | + K[1, 0] * K[0, 2] * K[2, 1] 269 | + K[2, 0] * K[0, 1] * K[1, 2] 270 | - K[2, 0] * K[0, 2] * K[1, 1], 271 | ] 272 | 273 | gamma = np.roots(coeff) 274 | tmp = np.sort_complex(gamma) 275 | gamma = tmp[[3, 0, 2, 1]] # convention 276 | 277 | k = ( 278 | k0 279 | * np.array( 280 | [alpha * np.ones(gamma.shape), beta * np.ones(gamma.shape), gamma] 281 | ).T 282 | ) 283 | v = np.zeros((4, 3), dtype=complex) 284 | 285 | for i, g in enumerate(gamma): 286 | # H = K + [ 287 | # [-beta2 - g ** 2, alpha * beta, alpha * g], 288 | # [alpha * beta, -alpha2 - g ** 2, beta * g], 289 | # [alpha * g, beta * g, -alpha2 - beta2], 290 | # ] 291 | v[i, :] = [ 292 | (K[1, 1] - alpha2 - g**2) * (K[2, 2] - alpha2 - beta2) 293 | - (K[1, 2] + beta * g) ** 2, 294 | (K[1, 2] + beta * g) * (K[2, 0] + alpha * g) 295 | - (K[0, 1] + alpha * beta) * (K[2, 2] - alpha2 - beta2), 296 | (K[0, 1] + alpha * beta) * (K[1, 2] + beta * g) 297 | - (K[0, 2] + alpha * g) * (K[1, 1] - alpha2 - g**2), 298 | ] 299 | 300 | p3 = v[0, :] 301 | p3 /= norm(p3) 302 | p4 = v[1, :] 303 | p4 /= norm(p4) 304 | p1 = np.cross(p3, k[0, :]) 305 | p1 /= norm(p1) 306 | p2 = np.cross(p4, k[1, :]) 307 | p2 /= norm(p2) 308 | 309 | p = np.array([p1, p2, p3, p4]) 310 | q = wl / (2.0 * np.pi * mu0 * c) * np.cross(k, p) 311 | 312 | return k, p, q 313 | 314 | nlayers = len(multilayer) 315 | d = np.asarray([l.thickness for l in multilayer]) 316 | 317 | # R and T are real, because they are powers 318 | # r and t are complex! 319 | R = np.zeros((2, 2, self.wls.size)) 320 | T = np.zeros((2, 2, self.wls.size)) 321 | 322 | epstot = np.zeros((3, 3, self.wls.size, nlayers), dtype=complex) 323 | for i, l in enumerate(multilayer): 324 | epstot[:, :, :, i] = l.mat.epsilonTensor(self.wls) 325 | 326 | for iwl, wl in enumerate(self.wls): 327 | epsilon = epstot[:, :, iwl, :] 328 | 329 | kx = 2 * np.pi / wl * np.sin(theta_inc_x) 330 | ky = 2 * np.pi / wl * np.sin(theta_inc_y) 331 | x = np.array([1, 0, 0], dtype=float) 332 | y = np.array([0, 1, 0], dtype=float) 333 | # z = np.array([0, 0, 1], dtype=float) 334 | k = np.zeros((4, 3, nlayers), dtype=complex) 335 | p = np.zeros((4, 3, nlayers), dtype=complex) 336 | q = np.zeros((4, 3, nlayers), dtype=complex) 337 | D = np.zeros((4, 4, nlayers), dtype=complex) 338 | P = np.zeros((4, 4, nlayers), dtype=complex) 339 | 340 | for i in range(nlayers): 341 | k[:, :, i], p[:, :, i], q[:, :, i] = find_roots( 342 | wl, epsilon[:, :, i], kx, ky 343 | ) 344 | D[:, :, i] = [ 345 | [ 346 | np.dot(x, p[0, :, i]), 347 | np.dot(x, p[1, :, i]), 348 | np.dot(x, p[2, :, i]), 349 | np.dot(x, p[3, :, i]), 350 | ], 351 | [ 352 | np.dot(y, q[0, :, i]), 353 | np.dot(y, q[1, :, i]), 354 | np.dot(y, q[2, :, i]), 355 | np.dot(y, q[3, :, i]), 356 | ], 357 | [ 358 | np.dot(y, p[0, :, i]), 359 | np.dot(y, p[1, :, i]), 360 | np.dot(y, p[2, :, i]), 361 | np.dot(y, p[3, :, i]), 362 | ], 363 | [ 364 | np.dot(x, q[0, :, i]), 365 | np.dot(x, q[1, :, i]), 366 | np.dot(x, q[2, :, i]), 367 | np.dot(x, q[3, :, i]), 368 | ], 369 | ] 370 | 371 | for i in range(1, nlayers - 1): 372 | P[:, :, i] = np.diag(np.exp(1j * k[:, 2, i] * d[i])) 373 | 374 | M = inv(D[:, :, 0]) 375 | for i in range(1, nlayers - 1): 376 | M = np.dot(M, np.dot(D[:, :, i], np.dot(P[:, :, i], inv(D[:, :, i])))) 377 | M = np.dot(M, D[:, :, -1]) 378 | 379 | deltaM = M[0, 0] * M[2, 2] - M[0, 2] * M[2, 0] 380 | 381 | # reflectance matrix (from yeh_electromagnetic) 382 | # r = [rss rsp; rps rpp] 383 | r = ( 384 | np.array( 385 | [ 386 | [ 387 | M[1, 0] * M[2, 2] - M[1, 2] * M[2, 0], 388 | M[3, 0] * M[2, 2] - M[3, 2] * M[2, 0], 389 | ], 390 | [ 391 | M[0, 0] * M[1, 2] - M[1, 0] * M[0, 2], 392 | M[0, 0] * M[3, 2] - M[3, 0] * M[0, 2], 393 | ], 394 | ], 395 | dtype=complex, 396 | ) 397 | / deltaM 398 | ) 399 | 400 | # transmittance matrix (from yeh_electromagnetic) 401 | # t = [tss tsp; tps tpp] 402 | t = np.array([[M[2, 2], -M[2, 0]], [-M[0, 2], M[0, 0]]]) / deltaM 403 | 404 | # P_t/P_inc = |E_t|**2/|E_inc|**2 . k_t_z/k_inc_z 405 | T[:, :, iwl] = (np.absolute(t) ** 2 * k[0, 2, -1] / k[0, 2, 0]).real 406 | 407 | # P_r/P_inc = |E_r|**2/|E_inc|**2 408 | R[:, :, iwl] = np.absolute(r) ** 2 409 | 410 | self.R = R 411 | self.T = T 412 | return self 413 | 414 | def __str__(self): 415 | return ( 416 | "ANISOTROPIC TRANSFER MATRIX SOLVER\n\n%s\n\ntheta inc x = %g\ntheta inc y = %g" 417 | % (self.multilayer.__str__(), self.theta_inc_x, self.theta_inc_y) 418 | ) 419 | -------------------------------------------------------------------------------- /EMpy/version.py: -------------------------------------------------------------------------------- 1 | version = "2.2.1" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Lorenzo Bolla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include requirements.txt 3 | recursive-include examples *.py *.dat 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .EXPORT_ALL_VARIABLES: 2 | .PHONY: help requirements-dev-upgrade 3 | 4 | SRC = EMpy 5 | SRC_TEST = tests 6 | REQUIREMENTS = requirements.txt requirements_dev.txt 7 | 8 | # Self-documenting Makefile 9 | # https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 10 | help: ## Print this help 11 | @grep -E '^[a-zA-Z][a-zA-Z0-9_-]*:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 12 | 13 | venv: ## Create venv for EMpy 14 | mkvirtualenv EMpy 15 | 16 | develop: upgrade-dev requirements-install ## Install project for development 17 | pip install -e . 18 | 19 | upgrade-dev: ## Upgrade packages for development 20 | pip install -U setuptools pip pip-tools pytest tox 21 | 22 | test: lint ## Run tests 23 | pytest 24 | 25 | tox: ## Run Python tests 26 | tox 27 | 28 | black: ## Run formatter 29 | black . 30 | 31 | lint: flake8 pyflakes mypy ## Run linters 32 | 33 | flake8: ## Run flake8 linter 34 | flake8 ${SRC} tests examples scripts 35 | 36 | pyflakes: ## Run pyflake linter 37 | pyflakes ${SRC} tests examples scripts 38 | 39 | mypy: ## Run mypy linter 40 | mypy ${SRC} tests examples scripts 41 | 42 | requirements: ${REQUIREMENTS} ## Create requirements files 43 | 44 | requirements.txt: setup.py 45 | pip-compile -v ${PIP_COMPILE_ARGS} --output-file requirements.txt setup.py 46 | 47 | %.txt: %.in 48 | pip-compile -v ${PIP_COMPILE_ARGS} --output-file $@ $< 49 | 50 | requirements-upgrade: PIP_COMPILE_ARGS += --upgrade 51 | requirements-upgrade: requirements ## Upgrade requirements 52 | 53 | requirements-sync: requirements ## Synchronize requirements 54 | pip-sync ${REQUIREMENTS} 55 | pip install -e . 56 | 57 | requirements-install: requirements ## Install requirements 58 | $(foreach req, ${REQUIREMENTS}, pip install --no-binary :all: -r $(req);) 59 | 60 | clean-repo: 61 | git diff --quiet HEAD # no pending commits 62 | git diff --cached --quiet HEAD # no unstaged changes 63 | git pull --ff-only # latest code 64 | 65 | release: requirements clean-repo ## Make a release (specify: PART=[major|minor|patch]) 66 | bump2version ${PART} 67 | git push 68 | git push --tags 69 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | EMpy - ElectroMagnetic Python 2 | ***************************** 3 | 4 | .. image:: https://github.com/lbolla/EMpy/actions/workflows/python-app.yml/badge.svg 5 | :target: https://github.com/lbolla/EMpy/actions/workflows/python-app.yml 6 | 7 | .. image:: https://badge.fury.io/py/ElectroMagneticPython.svg 8 | :target: https://badge.fury.io/py/ElectroMagneticPython 9 | 10 | `EMpy - ElectroMagnetic Python` is a suite of algorithms widely known 11 | and used in electromagnetic problems and optics: the transfer matrix 12 | algorithm, the rigorous coupled wave analysis algorithm and more. 13 | 14 | Run the examples in `examples/*` to have an idea how EMpy works. 15 | 16 | Visit http://lbolla.github.io/EMpy/ for more information. 17 | 18 | Installation 19 | ============ 20 | 21 | .. code-block:: bash 22 | 23 | $> pip install ElectromagneticPython 24 | 25 | Optionally, install `bvp`: 26 | 27 | .. code-block:: bash 28 | 29 | $> pip install scikits.bvp1lg 30 | 31 | Development 32 | =========== 33 | 34 | First, download the source code from https://github.com/lbolla/EMpy. 35 | 36 | Create a virtualenv with, e.g: 37 | 38 | .. code-block:: bash 39 | 40 | $> make venv 41 | 42 | Then, from inside a `virtualenv`, install dev environment with: 43 | 44 | .. code-block:: bash 45 | 46 | $> make develop 47 | 48 | Run tests with: 49 | 50 | .. code-block:: bash 51 | 52 | $> make test 53 | 54 | Upgrade dependencies with: 55 | 56 | .. code-block:: bash 57 | 58 | $> make requirements-upgrade 59 | $> make requirements-sync 60 | 61 | 62 | Release process 63 | =============== 64 | 65 | 1. Edit CHANGES 66 | 2. `make release PART=major|minor|patch` 67 | 68 | Citation 69 | ======== 70 | 71 | If you find EMpy useful in your job, please consider adding a citation. 72 | 73 | As reference: 74 | 75 | .. code-block:: 76 | 77 | Bolla, L. (2017). EMpy [Computer software]. https://github.com/lbolla/EMpy/ 78 | 79 | As text: 80 | 81 | .. code-block:: 82 | 83 | We used EMpy (version x.y.z) to complete our work. 84 | -------------------------------------------------------------------------------- /examples/ex_APRR.py: -------------------------------------------------------------------------------- 1 | """All-pass ring resonator example.""" 2 | 3 | import EMpy 4 | import numpy 5 | import pylab 6 | 7 | wls = numpy.linspace(1.5e-6, 1.6e-6, 1000) 8 | K = EMpy.devices.Coupler(wls, numpy.sqrt(0.08), 1.0) 9 | l = 2 * numpy.pi * 5e-6 10 | SWG = EMpy.devices.SWG(400, 220, 125).solve(wls) 11 | APRR = EMpy.devices.APRR(K, SWG.neff, l).solve() 12 | 13 | pylab.plot(wls, numpy.unwrap(numpy.angle(APRR.THRU)), "r.-") 14 | pylab.axis("tight") 15 | pylab.xlabel("wavelength /m") 16 | pylab.ylabel("phase") 17 | pylab.show() 18 | -------------------------------------------------------------------------------- /examples/ex_NRR.py: -------------------------------------------------------------------------------- 1 | """N-Ring resonators example.""" 2 | 3 | import EMpy 4 | import numpy 5 | import pylab 6 | 7 | wls = numpy.linspace(1.53e-6, 1.57e-6, 1000) 8 | 9 | Ks = [ 10 | EMpy.devices.Coupler(wls, numpy.sqrt(0.08), 1.0), 11 | EMpy.devices.Coupler(wls, numpy.sqrt(0.008), 1.0), 12 | EMpy.devices.Coupler(wls, numpy.sqrt(0.006), 1.0), 13 | EMpy.devices.Coupler(wls, numpy.sqrt(0.09), 1.0), 14 | ] 15 | 16 | R = 5e-6 17 | l1s = [numpy.pi * R, numpy.pi * R, numpy.pi * R] 18 | l2s = [numpy.pi * R, numpy.pi * R, numpy.pi * R] 19 | 20 | SWG = EMpy.devices.SWG(400, 220, 125).solve(wls) 21 | neffs = [SWG.neff, SWG.neff, SWG.neff] 22 | 23 | NRR = EMpy.devices.NRR(Ks, neffs, l1s, l2s).solve() 24 | 25 | pylab.plot( 26 | wls, 27 | 20 * numpy.log10(numpy.absolute(NRR.THRU)), 28 | "r.-", 29 | wls, 30 | 20 * numpy.log10(numpy.absolute(NRR.DROP)), 31 | "g.-", 32 | ) 33 | pylab.axis("tight") 34 | pylab.ylim([-30, 0]) 35 | pylab.xlabel("wavelength /m") 36 | pylab.ylabel("power /dB") 37 | pylab.legend(("THRU", "DROP")) 38 | pylab.show() 39 | -------------------------------------------------------------------------------- /examples/ex_RCWA.py: -------------------------------------------------------------------------------- 1 | """Rigorous Coupled Wave Analysis example.""" 2 | 3 | import numpy 4 | import pylab 5 | 6 | import EMpy 7 | from EMpy.materials import ( 8 | IsotropicMaterial, 9 | AnisotropicMaterial, 10 | RefractiveIndex, 11 | EpsilonTensor, 12 | ) 13 | 14 | 15 | alpha = 0.0 16 | delta = 0.0 17 | # psi = EMpy.utils.deg2rad(0.) # TM 18 | # psi = EMpy.utils.deg2rad(90.) # TE 19 | psi = EMpy.utils.deg2rad(70.0) # hybrid 20 | phi = EMpy.utils.deg2rad(90.0) 21 | 22 | LAMBDA = 1016e-9 # grating periodicity 23 | n = 2 # orders of diffraction 24 | 25 | UV6 = IsotropicMaterial("UV6", n0=RefractiveIndex(n0_const=1.560)) 26 | SiN = AnisotropicMaterial( 27 | "SiN", 28 | epsilon_tensor=EpsilonTensor( 29 | epsilon_tensor_const=EMpy.constants.eps0 30 | * EMpy.utils.euler_rotate( 31 | numpy.diag(numpy.asarray([1.8550, 1.8750, 1.9130]) ** 2), 32 | EMpy.utils.deg2rad(14), 33 | EMpy.utils.deg2rad(25), 34 | EMpy.utils.deg2rad(32), 35 | ) 36 | ), 37 | ) 38 | BPTEOS = IsotropicMaterial("BPTEOS", n0=RefractiveIndex(n0_const=1.448)) 39 | ARC1 = IsotropicMaterial("ARC1", n0=RefractiveIndex(n0_const=1.448)) 40 | 41 | EFF = IsotropicMaterial("EFF", n0=RefractiveIndex(n0_const=1.6)) 42 | 43 | multilayer1 = EMpy.utils.Multilayer( 44 | [ 45 | EMpy.utils.Layer(EMpy.materials.Air, numpy.inf), 46 | EMpy.utils.Layer(SiN, 226e-9), 47 | EMpy.utils.Layer(BPTEOS, 226e-9), 48 | EMpy.utils.BinaryGrating(SiN, BPTEOS, 0.659, LAMBDA, 123e-9), 49 | EMpy.utils.Layer(SiN, 219e-9), 50 | EMpy.utils.Layer(EMpy.materials.SiO2, 2188e-9), 51 | EMpy.utils.Layer(EMpy.materials.Si, numpy.inf), 52 | ] 53 | ) 54 | 55 | multilayer2 = EMpy.utils.Multilayer( 56 | [ 57 | EMpy.utils.Layer(EMpy.materials.Air, numpy.inf), 58 | EMpy.utils.Layer(SiN, 226e-9), 59 | EMpy.utils.Layer(BPTEOS, 226e-9), 60 | EMpy.utils.Layer(IsotropicMaterial(n0=RefractiveIndex(n0_const=1.6)), 123e-9), 61 | EMpy.utils.Layer(SiN, 219e-9), 62 | EMpy.utils.Layer(EMpy.materials.SiO2, 2188e-9), 63 | EMpy.utils.Layer(EMpy.materials.Si, numpy.inf), 64 | ] 65 | ) 66 | 67 | wls = numpy.linspace(1.45e-6, 1.75e-6, 301) 68 | 69 | solution1 = EMpy.RCWA.AnisotropicRCWA(multilayer1, alpha, delta, psi, phi, n).solve(wls) 70 | solution2 = EMpy.RCWA.AnisotropicRCWA(multilayer2, alpha, delta, psi, phi, n).solve(wls) 71 | 72 | um = 1e-6 73 | pylab.plot( 74 | # wls / um, solution1.DEO1[n, :], 'k.-', 75 | # wls / um, solution1.DEO3[n, :], 'r.-', 76 | wls / um, 77 | solution1.DEE1[n, :], 78 | "b.-", 79 | wls / um, 80 | solution1.DEE3[n, :], 81 | "g.-", 82 | # wls / um, solution2.DEO1[n, :], 'k--', 83 | # wls / um, solution2.DEO3[n, :], 'r--', 84 | wls / um, 85 | solution2.DEE1[n, :], 86 | "b--", 87 | wls / um, 88 | solution2.DEE3[n, :], 89 | "g--", 90 | ) 91 | pylab.xlabel("wavelength [um]") 92 | pylab.ylabel("diffraction efficiency") 93 | pylab.legend(("DEO1", "DEO3", "DEE1", "DEE3")) 94 | pylab.axis("tight") 95 | pylab.ylim([0, 0.15]) 96 | pylab.show() 97 | -------------------------------------------------------------------------------- /examples/ex_RCWA_2.py: -------------------------------------------------------------------------------- 1 | """Rigorous Coupled Wave Analysis example. 2 | 3 | Inspired by Moharam, "Formulation for stable and efficient implementation of the rigorous coupled-wave analysis of 4 | binary gratings", JOSA A, 12(5), 1995 5 | """ 6 | 7 | import numpy 8 | import pylab 9 | 10 | import EMpy 11 | from EMpy.materials import IsotropicMaterial, RefractiveIndex 12 | 13 | 14 | alpha = EMpy.utils.deg2rad(10.0) 15 | delta = EMpy.utils.deg2rad(0.0) 16 | psi = EMpy.utils.deg2rad(0.0) # TE 17 | phi = EMpy.utils.deg2rad(90.0) 18 | 19 | wl = numpy.array([1.55e-6]) 20 | ds = numpy.linspace(0.0, 5.0, 100) * wl 21 | LAMBDA = 10 * wl 22 | 23 | n = 3 # orders of diffraction 24 | 25 | Top = IsotropicMaterial("Top", n0=RefractiveIndex(n0_const=1.0)) 26 | Bottom = IsotropicMaterial("Bottom", n0=RefractiveIndex(n0_const=2.04)) 27 | 28 | solutions = [] 29 | for d in ds: 30 | multilayer = EMpy.utils.Multilayer( 31 | [ 32 | EMpy.utils.Layer(Top, numpy.inf), 33 | EMpy.utils.BinaryGrating(Top, Bottom, 0.3, LAMBDA, d), 34 | EMpy.utils.Layer(Bottom, numpy.inf), 35 | ] 36 | ) 37 | 38 | solution = EMpy.RCWA.IsotropicRCWA(multilayer, alpha, delta, psi, phi, n).solve(wl) 39 | solutions.append(solution) 40 | 41 | DE1 = numpy.zeros(len(solutions)) 42 | DE3 = numpy.zeros(len(solutions)) 43 | for ss, s in enumerate(solutions): 44 | DE1[ss] = s.DE1[n, 0] 45 | DE3[ss] = s.DE3[n, 0] 46 | 47 | pylab.plot(ds / wl, DE1[:], "k.-", ds / wl, DE3[:], "r.-") 48 | pylab.xlabel("normalized groove depth") 49 | pylab.ylabel("diffraction efficiency") 50 | pylab.legend(("DE1", "DE3")) 51 | pylab.axis("tight") 52 | pylab.ylim([0, 1]) 53 | pylab.show() 54 | -------------------------------------------------------------------------------- /examples/ex_RCWA_3.py: -------------------------------------------------------------------------------- 1 | """Rigorous Coupled Wave Analysis example.""" 2 | 3 | import numpy 4 | import pylab 5 | 6 | import EMpy 7 | from EMpy.materials import IsotropicMaterial, RefractiveIndex 8 | 9 | 10 | alpha = EMpy.utils.deg2rad(30.0) 11 | delta = EMpy.utils.deg2rad(45.0) 12 | psi = EMpy.utils.deg2rad(0.0) # TE 13 | phi = EMpy.utils.deg2rad(90.0) 14 | 15 | wls = numpy.linspace(1.5495e-6, 1.550e-6, 101) 16 | 17 | LAMBDA = 1.0e-6 # grating periodicity 18 | n = 3 # orders of diffraction 19 | 20 | Top = IsotropicMaterial("Top", n0=RefractiveIndex(n0_const=1.0)) 21 | Bottom = IsotropicMaterial("Bottom", n0=RefractiveIndex(n0_const=3.47)) 22 | 23 | multilayer = EMpy.utils.Multilayer( 24 | [ 25 | EMpy.utils.Layer(Top, numpy.inf), 26 | EMpy.utils.BinaryGrating(Top, Bottom, 0.4, LAMBDA, 0.01), 27 | EMpy.utils.Layer(Bottom, numpy.inf), 28 | ] 29 | ) 30 | 31 | solution = EMpy.RCWA.IsotropicRCWA(multilayer, alpha, delta, psi, phi, n).solve(wls) 32 | 33 | pylab.plot( 34 | wls, 35 | solution.DE1[n, :], 36 | "ko-", 37 | wls, 38 | solution.DE3[n, :], 39 | "ro-", 40 | wls, 41 | solution.DE1[n - 1, :], 42 | "kx-", 43 | wls, 44 | solution.DE3[n - 1, :], 45 | "rx-", 46 | wls, 47 | solution.DE1[n + 1, :], 48 | "k.-", 49 | wls, 50 | solution.DE3[n + 1, :], 51 | "r.-", 52 | ) 53 | pylab.xlabel("wavelength /m") 54 | pylab.ylabel("diffraction efficiency") 55 | pylab.legend(("DE1:0", "DE3:0", "DE1:-1", "DE3:-1", "DE1:+1", "DE3:+1")) 56 | pylab.axis("tight") 57 | pylab.ylim([0, 1]) 58 | pylab.show() 59 | -------------------------------------------------------------------------------- /examples/ex_SRR.py: -------------------------------------------------------------------------------- 1 | """Single ring resonator example.""" 2 | 3 | import EMpy 4 | import numpy 5 | import pylab 6 | 7 | wls = numpy.linspace(1.53e-6, 1.56e-6, 1000) 8 | K1 = EMpy.devices.Coupler(wls, numpy.sqrt(0.08), 1.0) 9 | K2 = EMpy.devices.Coupler(wls, numpy.sqrt(0.08), 1.0) 10 | l1 = numpy.pi * 5e-6 11 | l2 = numpy.pi * 5e-6 12 | SWG = EMpy.devices.SWG(488, 220, 25).solve(wls) 13 | SRR = EMpy.devices.SRR(K1, K2, SWG.neff, l1, l2).solve() 14 | 15 | pylab.plot(wls, numpy.absolute(SRR.THRU), "r.-", wls, numpy.absolute(SRR.DROP), "g.-") 16 | pylab.axis("tight") 17 | pylab.ylim([0, 1]) 18 | pylab.xlabel("wavelength /m") 19 | pylab.ylabel("power") 20 | pylab.legend(("THRU", "DROP")) 21 | pylab.show() 22 | -------------------------------------------------------------------------------- /examples/ex_laser_etch_monitor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Transfer matrix example: Laser Etch Monitor/End-point detection. 4 | Built for Electromagnetic Python by Lorenzo Bolla: 5 | http://lbolla.github.io/EMpy/ 6 | 7 | Simulate a laser etch monitor (aka. laser endpoint detection): 8 | Calculate the reflectivity of a thin-film stack as the stack is 9 | progressively thinned by etching from one side. Finally, plot 10 | reflectivity vs. etch depth at a single wavelength, as is often 11 | used to determine end-points during reactive ion/plasma etching. 12 | For more info, see Oxford Plasma's page on the technique: 13 | http://www.oxfordplasma.de/technols/li.htm 14 | 15 | Requires file `nk.py` for wavelength-dependent refractive index/loss models. 16 | 17 | Author: Demis D. John, March 2017 (demis.john@gmail.com) 18 | """ 19 | 20 | # to make copies of Layer objects, instead of mutable references 21 | from copy import copy, deepcopy 22 | 23 | # for progress bar (sys.stdout) 24 | import sys 25 | 26 | import pylab 27 | import numpy 28 | 29 | import EMpy 30 | 31 | # file of refractive indices, takes wavelengths in Microns!! 32 | import nk 33 | 34 | 35 | # Simulation Parameters 36 | # laser monitor wavelength 37 | wl_lasermon = 670e-9 38 | # how much to etch before acquiring new laser monitor reflectivity 39 | EtchStep = 10e-9 40 | 41 | wls = numpy.array([wl_lasermon - 1e-9, wl_lasermon, wl_lasermon + 1e-9]) 42 | theta_inc = EMpy.utils.deg2rad(0) # incidence angle 43 | 44 | 45 | # Define some helper functions 46 | 47 | 48 | def find_nearest(a, a0): 49 | """Return element in ndArray `a` that has value closest to the 50 | scalar value `a0`.""" 51 | idx = numpy.abs(a - a0).argmin() 52 | return a.flat[idx] 53 | 54 | 55 | def arg_find_nearest(a, a0): 56 | """Return index to element in ndArray `a` that has value closest 57 | to the scalar value `a0`.""" 58 | idx = numpy.abs(a - a0).argmin() 59 | return idx 60 | 61 | 62 | def count_noninf(multilayer): 63 | """Return number of non-infinite layers in an EMpy Multilayer 64 | object.""" 65 | out = 0 66 | for x in multilayer: 67 | out = out + 0 if numpy.isinf(x.thickness) else out + 1 68 | return out 69 | 70 | 71 | def arg_inf(multilayer): 72 | """Return index to layers with infinite-thickness in an EMpy Multilayer object.""" 73 | out = [] 74 | for ix, x in enumerate(multilayer): 75 | if numpy.isinf(x.thickness): 76 | out.append(ix) 77 | return out 78 | 79 | 80 | # Define some materials 81 | 82 | # Define RefractiveIndex functions, then Material objects. 83 | n_air = 1.0 # constant vs. wavelength 84 | mat_air = EMpy.materials.IsotropicMaterial( 85 | "air", EMpy.materials.RefractiveIndex(n0_const=n_air) 86 | ) 87 | 88 | n_SiN = 1.997 # Silicon Nitride (Si3N4) 89 | mat_SiN = EMpy.materials.IsotropicMaterial( 90 | "Si3N4", EMpy.materials.RefractiveIndex(n0_const=n_SiN) 91 | ) 92 | 93 | 94 | # RIX functions from file, taking wavelength in microns: 95 | # Covert to microns, request loss as complex refractive index 96 | def n_GaAs(w): 97 | return nk.GaAs_interp(w * 1e6, k=True) 98 | 99 | 100 | mat_GaAs = EMpy.materials.IsotropicMaterial( 101 | "GaAs", EMpy.materials.RefractiveIndex(n0_func=n_GaAs) 102 | ) 103 | 104 | 105 | # Function from file, AlGaAs with 95% Aluminum 106 | def n_AlGaAs95(w): 107 | return nk.AlGaAs_interp(0.95, w * 1e6, k=True) 108 | 109 | 110 | mat_AlGaAs95 = EMpy.materials.IsotropicMaterial( 111 | "Al95Ga05As", EMpy.materials.RefractiveIndex(n0_func=n_AlGaAs95) 112 | ) 113 | 114 | 115 | # DBR mirror periods, at 1/4-wavelength thicknesses 116 | wl_center = 1100e-9 117 | d_GaAs_DBR = wl_center / n_GaAs(wl_center).real / 4 118 | d_AlGaAs_DBR = wl_center / n_AlGaAs95(wl_center).real / 4 119 | 120 | 121 | # Build the Multilayer stack 122 | # Build from top to bottom - ie. topmost layer (closest to laser 123 | # monitor) first, substrate last. Must include infinite-thickness 124 | # layers on top & bottom for air & substrate, respectively. 125 | 126 | Layer = EMpy.utils.Layer # shortcut 127 | 128 | # define the layers (material, thickness) we will use in the stack: 129 | air = Layer(mat_air, numpy.inf) 130 | SiN = Layer(mat_SiN, 300e-9) 131 | GaAs_DBR = Layer(mat_GaAs, d_GaAs_DBR) 132 | AlGaAs_DBR = Layer(mat_AlGaAs95, d_AlGaAs_DBR) 133 | GaAs_core = Layer(mat_GaAs, 551.75e-9) 134 | AlGaAs_spacer = Layer(mat_AlGaAs95, 500e-9) 135 | GaAs_spacer = Layer(mat_GaAs, 300e-9) 136 | GaAs_substrate = Layer(mat_GaAs, numpy.inf) 137 | 138 | # Use lists to enable periodic structures etc. 139 | # Make sure to include infinite-thickness layers on ends 140 | layers_ = ( 141 | [air] 142 | + [SiN] 143 | + 5 * [GaAs_DBR, AlGaAs_DBR] 144 | + [GaAs_core] 145 | + 5 * [AlGaAs_DBR, GaAs_DBR] 146 | + [AlGaAs_spacer] 147 | + [GaAs_spacer] 148 | + [GaAs_substrate] 149 | ) 150 | 151 | # Create EMpy MultiLayer stack. 152 | # Must dereference the lists from each other via copy(), otherwise altering 153 | # one layer during etching also affects other repeated Layers 154 | layers = EMpy.utils.Multilayer([copy(l) for l in layers_]) 155 | 156 | 157 | # setup etching loop 158 | EtchStep_current = EtchStep # how much left to etch in current loop iteration 159 | go = True # while loop switch 160 | i = -1 # while loop counter 161 | etchedlayers = [] # save stacks of etched layers 162 | solutions = [] # IsotropicTransferMatrix object storing R/T solutions 163 | EtchSteps = [] # x-axis data 164 | Rlaser = [] # y-axis data - reflectivity 165 | RefrIdx = [] # y-axis data - refractive index 166 | # get index to laser-monitor wavelength in `wls` array 167 | wlidx = arg_find_nearest(wls, wl_lasermon) 168 | # 0 if etching away from first layer in list, -1 if etching from last 169 | # layer appended 170 | idxtemp = 0 171 | 172 | print("Etching...") 173 | while go is True: 174 | # keep reducing thickness/removing layers until last layer is too thin 175 | i = i + 1 176 | 177 | # print a small progress bar 178 | sys.stdout.write(".") 179 | sys.stdout.flush() 180 | 181 | if count_noninf(layers) > 0: 182 | # at least one infinite-thickness layer should still be left 183 | 184 | if i <= 0: 185 | # first iteration: analyze unetched structure 186 | EtchStep_current = 0.0 187 | indexno = idxtemp 188 | else: 189 | # point to non-infinite layers for etching: 190 | while numpy.isinf(layers[idxtemp].thickness): 191 | idxtemp = idxtemp + 1 # assumes etching from 1st layer in list 192 | indexno = idxtemp 193 | 194 | if layers[indexno].thickness <= EtchStep_current: 195 | # next layer is thinner than etch step 196 | # Reduce etch step + remove this layer: 197 | EtchStep_current = EtchStep_current - layers[indexno].thickness 198 | layers.pop(indexno) 199 | 200 | elif layers[indexno].thickness > EtchStep_current: 201 | # etch increment ends within next layer 202 | # reduce layer thickness & solve & save data points: 203 | layers[indexno].thickness = layers[indexno].thickness - EtchStep_current 204 | # add this layer stack to the list 205 | etchedlayers.append(deepcopy(layers)) 206 | # get RefrIndex in this layer 207 | RefrIdx.append(etchedlayers[-1][idxtemp].mat.n(wl_lasermon).real) 208 | if i <= 0: 209 | # for 1st iteration: unetched layer 210 | EtchSteps.append(0.0) 211 | else: 212 | EtchSteps.append(EtchSteps[-1] + EtchStep) # Add x-axis point 213 | EtchStep_current = EtchStep # reset EtchStep_current 214 | 215 | # solve for reflectivity at laser monitor wavelength 216 | solutions.append( 217 | EMpy.transfer_matrix.IsotropicTransferMatrix( 218 | etchedlayers[-1], theta_inc 219 | ).solve(wls) 220 | ) 221 | Rlaser.append(solutions[-1].Rs[wlidx]) 222 | else: 223 | # No non-infinte layers left, end the loop 224 | go = False 225 | 226 | print("\n") 227 | 228 | 229 | # Plots: 230 | fig1, [ax1, ax2] = pylab.subplots(nrows=2, ncols=1, sharex=True) 231 | ax1.set_title(r"Reflectivity at $\lambda = %0.1fnm$" % (wls[wlidx] * 1e9)) 232 | 233 | # plot refractive index vs. depth 234 | ax1.plot(numpy.array(EtchSteps) * 1e9, RefrIdx, "-g") 235 | ax1.set_ylabel("Refractive Index") 236 | ax1.grid(True) 237 | 238 | # plot reflectivity vs. depth 239 | ax2.plot(numpy.array(EtchSteps) * 1e9, numpy.array(Rlaser) * 100, "-") 240 | ax2.set_ylabel("Laser Reflectivity (%)") 241 | ax2.set_xlabel("Etch Depth (nm)") 242 | ax2.grid(True) 243 | 244 | fig1.show() 245 | pylab.show() 246 | -------------------------------------------------------------------------------- /examples/ex_modesolver.py: -------------------------------------------------------------------------------- 1 | """Fully vectorial finite-difference mode solver example.""" 2 | 3 | import numpy 4 | import EMpy 5 | import pylab 6 | 7 | 8 | def epsfunc(x_, y_): 9 | """Return a matrix describing a 2d material. 10 | 11 | :param x_: x values 12 | :param y_: y values 13 | :return: 2d-matrix 14 | """ 15 | xx, yy = numpy.meshgrid(x_, y_) 16 | return numpy.where( 17 | (numpy.abs(xx.T - 1.24e-6) <= 0.24e-6) * (numpy.abs(yy.T - 1.11e-6) <= 0.11e-6), 18 | 3.4757**2, 19 | 1.446**2, 20 | ) 21 | 22 | 23 | wl = 1.55e-6 24 | x = numpy.linspace(0, 2.48e-6, 125) 25 | y = numpy.linspace(0, 2.22e-6, 112) 26 | 27 | neigs = 2 28 | tol = 1e-8 29 | boundary = "0000" 30 | 31 | solver = EMpy.modesolvers.FD.VFDModeSolver(wl, x, y, epsfunc, boundary).solve( 32 | neigs, tol 33 | ) 34 | 35 | fig = pylab.figure() 36 | 37 | fig.add_subplot(1, 3, 1) 38 | Ex = numpy.transpose(solver.modes[0].get_field("Ex", x, y)) 39 | pylab.contourf(x, y, abs(Ex), 50) 40 | pylab.title("Ex") 41 | 42 | fig.add_subplot(1, 3, 2) 43 | Ey = numpy.transpose(solver.modes[0].get_field("Ey", x, y)) 44 | pylab.contourf(x, y, abs(Ey), 50) 45 | pylab.title("Ey") 46 | 47 | fig.add_subplot(1, 3, 3) 48 | Ez = numpy.transpose(solver.modes[0].get_field("Ez", x, y)) 49 | pylab.contourf(x, y, abs(Ez), 50) 50 | pylab.title("Ez") 51 | 52 | fig.add_subplot(1, 3, 1) 53 | Hx = numpy.transpose(solver.modes[0].get_field("Hx", x, y)) 54 | pylab.contourf(x, y, abs(Hx), 50) 55 | pylab.title("Hx") 56 | 57 | fig.add_subplot(1, 3, 2) 58 | Hy = numpy.transpose(solver.modes[0].get_field("Hy", x, y)) 59 | pylab.contourf(x, y, abs(Hy), 50) 60 | pylab.title("Hy") 61 | 62 | fig.add_subplot(1, 3, 3) 63 | Hz = numpy.transpose(solver.modes[0].get_field("Hz", x, y)) 64 | pylab.contourf(x, y, abs(Hz), 50) 65 | pylab.title("Hz") 66 | 67 | pylab.show() 68 | -------------------------------------------------------------------------------- /examples/ex_modesolver_2.py: -------------------------------------------------------------------------------- 1 | """Fully vectorial finite-difference mode solver example.""" 2 | 3 | import EMpy 4 | import pylab 5 | 6 | mat1 = EMpy.materials.IsotropicMaterial( 7 | "SiN", n0=EMpy.materials.RefractiveIndex(n0_const=1.97) 8 | ) 9 | mat2 = EMpy.materials.IsotropicMaterial( 10 | "Si", n0=EMpy.materials.RefractiveIndex(n0_const=3.4757) 11 | ) 12 | 13 | l1 = EMpy.utils.Layer(mat1, 4.1e-6) 14 | l21 = EMpy.utils.Layer(mat1, 2e-6) 15 | l22 = EMpy.utils.Layer(mat2, 0.1e-6) 16 | 17 | w = 0.5e-6 18 | 19 | d = EMpy.utils.EMpy.utils.CrossSection( 20 | [ 21 | EMpy.utils.Slice(2e-6, [l1]), 22 | EMpy.utils.Slice(w, [l21, l22, l21]), 23 | EMpy.utils.Slice(2e-6, [l1]), 24 | ] 25 | ) 26 | 27 | nx_points_per_region = (20, 10, 20) 28 | ny_points_per_region = (20, 20, 20) 29 | 30 | (X, Y) = d.grid(nx_points_per_region, ny_points_per_region) 31 | eps = d.epsfunc(X, Y, 1.55e-6) 32 | 33 | 34 | def epsfunc(x, y): 35 | return d.epsfunc(x, y, wl) 36 | 37 | 38 | wl = 1.55e-6 39 | 40 | neigs = 2 41 | tol = 1e-8 42 | boundary = "0000" 43 | 44 | solver = EMpy.modesolvers.FD.VFDModeSolver(wl, X, Y, epsfunc, boundary).solve( 45 | neigs, tol 46 | ) 47 | fig = pylab.figure() 48 | fig.add_subplot(1, 3, 1) 49 | pylab.contourf(abs(solver.modes[0].Ex), 50) 50 | pylab.title("Ex") 51 | fig.add_subplot(1, 3, 2) 52 | pylab.contourf(abs(solver.modes[0].Ey), 50) 53 | pylab.title("Ey") 54 | fig.add_subplot(1, 3, 3) 55 | pylab.contourf(abs(solver.modes[0].Ez), 50) 56 | pylab.title("Ez") 57 | pylab.show() 58 | -------------------------------------------------------------------------------- /examples/ex_modesolver_3.py: -------------------------------------------------------------------------------- 1 | """Fully vectorial finite-difference mode solver example.""" 2 | 3 | import numpy 4 | import EMpy 5 | import pylab 6 | 7 | 8 | def epsfunc(x_, y_): 9 | """Similar to ex_modesolver.py, but using anisotropic eps.""" 10 | eps = numpy.zeros((len(x_), len(y_), 5)) 11 | for ix, xx in enumerate(x_): 12 | for iy, yy in enumerate(y_): 13 | if abs(xx - 1.24e-6) <= 0.24e-6 and abs(yy - 1.11e-6) <= 0.11e-6: 14 | a = 3.4757**2 15 | b = 1 # some xy value 16 | # eps_xx, xy, yx, yy, zz 17 | eps[ix, iy, :] = [a, b, b, a, a] 18 | else: 19 | a = 1.446**2 20 | # isotropic 21 | eps[ix, iy, :] = [a, 0, 0, a, a] 22 | return eps 23 | 24 | 25 | wl = 1.55e-6 26 | x = numpy.linspace(0, 2.48e-6, 125) 27 | y = numpy.linspace(0, 2.22e-6, 112) 28 | 29 | neigs = 2 30 | tol = 1e-8 31 | boundary = "0000" 32 | 33 | solver = EMpy.modesolvers.FD.VFDModeSolver(wl, x, y, epsfunc, boundary).solve( 34 | neigs, tol 35 | ) 36 | 37 | fig = pylab.figure() 38 | fig.add_subplot(1, 3, 1) 39 | pylab.contourf(abs(solver.modes[0].Ex), 50) 40 | pylab.title("Ex") 41 | fig.add_subplot(1, 3, 2) 42 | pylab.contourf(abs(solver.modes[0].Ey), 50) 43 | pylab.title("Ey") 44 | fig.add_subplot(1, 3, 3) 45 | pylab.contourf(abs(solver.modes[0].Ez), 50) 46 | pylab.title("Ez") 47 | pylab.show() 48 | -------------------------------------------------------------------------------- /examples/ex_modesolver_4.py: -------------------------------------------------------------------------------- 1 | """Semi-vectorial finite-difference mode solver example.""" 2 | 3 | import numpy 4 | import EMpy 5 | import pylab 6 | 7 | 8 | def epsfunc(x_, y_): 9 | """Return a matrix describing a 2d material. 10 | 11 | :param x_: x values 12 | :param y_: y values 13 | :return: 2d-matrix 14 | """ 15 | xx, yy = numpy.meshgrid(x_, y_) 16 | return numpy.where( 17 | (numpy.abs(xx.T - 1.24e-6) <= 0.24e-6) * (numpy.abs(yy.T - 1.11e-6) <= 0.11e-6), 18 | 3.4757**2, 19 | 1.446**2, 20 | ) 21 | 22 | 23 | wl = 1.55e-6 24 | x = numpy.linspace(0, 2.48e-6, 125) 25 | y = numpy.linspace(0, 2.22e-6, 112) 26 | 27 | neigs = 2 28 | tol = 1e-8 29 | boundary = "0000" 30 | 31 | solver = EMpy.modesolvers.FD.SVFDModeSolver(wl, x, y, epsfunc, boundary).solve( 32 | neigs, tol 33 | ) 34 | 35 | fig = pylab.figure() 36 | fig.add_subplot(1, 2, 1) 37 | pylab.contourf(abs(solver.Ex[0]), 50) 38 | pylab.title("Ex first mode") 39 | fig.add_subplot(1, 2, 2) 40 | pylab.contourf(abs(solver.Ex[1]), 50) 41 | pylab.title("Ex second mode") 42 | pylab.show() 43 | -------------------------------------------------------------------------------- /examples/ex_modesolver_dch.py: -------------------------------------------------------------------------------- 1 | """Fully vectorial finite-difference mode solver example. 2 | 3 | Flexible mode solver example for fundamental modes 4 | by David Hutchings, School of Engineering, University of Glasgow 5 | David.Hutchings@glasgow.ac.uk 6 | """ 7 | 8 | import numpy 9 | import EMpy 10 | import matplotlib.pyplot as plt 11 | from matplotlib import ticker 12 | from scipy import signal 13 | 14 | """Define cross-section geometry of simulation 15 | each rectangular element tuple contains 16 | ("label",xmin,ymin,xmax,ymax,eps=refractive index squared) 17 | first element tuple defines simulation extent 18 | units should match units of wavelength wl 19 | """ 20 | geom = ( 21 | ("base", -0.8, -0.8, 0.8, 1.0, 1.0**2), 22 | ("substrate", -0.8, -0.8, 0.8, 0.0, 1.5**2), 23 | ("core1", -0.30, 0.0, 0.0, 0.34, 3.4**2), 24 | ("core2", 0.0, 0.0, 0.30, 0.145, 3.4**2), 25 | ) 26 | 27 | wl = 1.55 28 | nx, ny = 161, 181 29 | 30 | 31 | def epsfunc(x_, y_): 32 | """Return a matrix describing a 2d material. 33 | 34 | :param x_: x values 35 | :param y_: y values 36 | :return: 2d-matrix 37 | """ 38 | # xx, yy = numpy.meshgrid(x_, y_) 39 | working = geom[0][5] * numpy.ones((x_.size, y_.size)) 40 | for i in range(1, len(geom)): 41 | ixmin = numpy.searchsorted(x_, geom[i][1], side="left") 42 | iymin = numpy.searchsorted(y_, geom[i][2], side="left") 43 | ixmax = numpy.searchsorted(x_, geom[i][3], side="right") 44 | iymax = numpy.searchsorted(y_, geom[i][4], side="right") 45 | working[ixmin:ixmax, iymin:iymax] = geom[i][5] 46 | 47 | return working 48 | 49 | 50 | x = numpy.linspace(geom[0][1], geom[0][3], nx) 51 | y = numpy.linspace(geom[0][2], geom[0][4], ny) 52 | 53 | neigs = 2 54 | tol = 1e-8 55 | boundary = "0000" 56 | 57 | solver = EMpy.modesolvers.FD.VFDModeSolver(wl, x, y, epsfunc, boundary).solve( 58 | neigs, tol 59 | ) 60 | 61 | levls = numpy.geomspace(1.0 / 32.0, 1.0, num=11) 62 | levls2 = numpy.geomspace(1.0 / 1024.0, 1.0, num=11) 63 | xe = signal.convolve(x, [0.5, 0.5])[1:-1] 64 | ye = signal.convolve(y, [0.5, 0.5])[1:-1] 65 | 66 | 67 | def geom_outline(): 68 | ax.set_xlim(geom[0][1], geom[0][3]) 69 | ax.set_ylim(geom[0][2], geom[0][4]) 70 | for i in range(1, len(geom)): 71 | plt.hlines(geom[i][2], geom[i][1], geom[i][3]) 72 | plt.hlines(geom[i][4], geom[i][1], geom[i][3]) 73 | plt.vlines(geom[i][1], geom[i][2], geom[i][4]) 74 | plt.vlines(geom[i][3], geom[i][2], geom[i][4]) 75 | return 76 | 77 | 78 | print(solver.modes[0].neff) 79 | fmax = abs(solver.modes[0].Ex).max() 80 | fig = plt.figure() 81 | ax = fig.add_subplot(2, 3, 1) 82 | plt.contour( 83 | xe, 84 | ye, 85 | abs(solver.modes[0].Ex.T), 86 | fmax * levls, 87 | cmap="jet", 88 | locator=ticker.LogLocator(), 89 | ) 90 | plt.title("Ex") 91 | geom_outline() 92 | ax = fig.add_subplot(2, 3, 2) 93 | plt.contour( 94 | xe, 95 | ye, 96 | abs(solver.modes[0].Ey.T), 97 | fmax * levls, 98 | cmap="jet", 99 | locator=ticker.LogLocator(), 100 | ) 101 | plt.title("Ey") 102 | geom_outline() 103 | ax = fig.add_subplot(2, 3, 3) 104 | plt.contour( 105 | xe, 106 | ye, 107 | abs(solver.modes[0].Ez.T), 108 | fmax * levls, 109 | cmap="jet", 110 | locator=ticker.LogLocator(), 111 | ) 112 | plt.title("Ez") 113 | geom_outline() 114 | fmax = abs(solver.modes[0].Hy).max() 115 | ax = fig.add_subplot(2, 3, 4) 116 | plt.contour( 117 | x, 118 | y, 119 | abs(solver.modes[0].Hx.T), 120 | fmax * levls, 121 | cmap="jet", 122 | locator=ticker.LogLocator(), 123 | ) 124 | plt.title("Hx") 125 | geom_outline() 126 | ax = fig.add_subplot(2, 3, 5) 127 | plt.contour( 128 | x, 129 | y, 130 | abs(solver.modes[0].Hy.T), 131 | fmax * levls, 132 | cmap="jet", 133 | locator=ticker.LogLocator(), 134 | ) 135 | plt.title("Hy") 136 | geom_outline() 137 | ax = fig.add_subplot(2, 3, 6) 138 | plt.contour( 139 | x, 140 | y, 141 | abs(solver.modes[0].Hz.T), 142 | fmax * levls, 143 | cmap="jet", 144 | locator=ticker.LogLocator(), 145 | ) 146 | plt.title("Hz") 147 | geom_outline() 148 | plt.show() 149 | 150 | ExatH = signal.convolve2d(solver.modes[0].Ex, [[0.25, 0.25], [0.25, 0.25]]) 151 | EyatH = signal.convolve2d(solver.modes[0].Ey, [[0.25, 0.25], [0.25, 0.25]]) 152 | EzatH = signal.convolve2d(solver.modes[0].Ez, [[0.25, 0.25], [0.25, 0.25]]) 153 | # Stokes parameters 154 | q1 = ExatH * numpy.conjugate(solver.modes[0].Hy) 155 | q2 = EyatH * numpy.conjugate(solver.modes[0].Hx) 156 | q3 = EyatH * numpy.conjugate(solver.modes[0].Hy) 157 | q4 = ExatH * numpy.conjugate(solver.modes[0].Hx) 158 | 159 | S0 = q1.real - q2.real 160 | S1 = q1.real + q2.real 161 | S2 = q3.real - q4.real 162 | S3 = q3.imag + q4.imag 163 | denom = S0.sum() 164 | print("ave S1=", S1.sum() / denom) 165 | print("ave S2=", S2.sum() / denom) 166 | print("ave S3=", S3.sum() / denom) 167 | 168 | fmax = abs(S0).max() 169 | fig = plt.figure() 170 | ax = fig.add_subplot(2, 2, 1) 171 | plt.contour(x, y, abs(S0.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 172 | plt.title("S0") 173 | geom_outline() 174 | ax = fig.add_subplot(2, 2, 2) 175 | plt.contour(x, y, abs(S1.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 176 | plt.title("S1") 177 | geom_outline() 178 | ax = fig.add_subplot(2, 2, 3) 179 | plt.contour(x, y, abs(S2.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 180 | plt.title("S2") 181 | geom_outline() 182 | ax = fig.add_subplot(2, 2, 4) 183 | plt.contour(x, y, abs(S3.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 184 | plt.title("S3") 185 | geom_outline() 186 | plt.show() 187 | 188 | print(solver.modes[1].neff) 189 | fmax = abs(solver.modes[1].Ey).max() 190 | fig = plt.figure() 191 | ax = fig.add_subplot(2, 3, 1) 192 | plt.contour( 193 | xe, 194 | ye, 195 | abs(solver.modes[1].Ex.T), 196 | fmax * levls, 197 | cmap="jet", 198 | locator=ticker.LogLocator(), 199 | ) 200 | plt.title("Ex") 201 | geom_outline() 202 | ax = fig.add_subplot(2, 3, 2) 203 | plt.contour( 204 | xe, 205 | ye, 206 | abs(solver.modes[1].Ey.T), 207 | fmax * levls, 208 | cmap="jet", 209 | locator=ticker.LogLocator(), 210 | ) 211 | plt.title("Ey") 212 | geom_outline() 213 | ax = fig.add_subplot(2, 3, 3) 214 | plt.contour( 215 | xe, 216 | ye, 217 | abs(solver.modes[1].Ez.T), 218 | fmax * levls, 219 | cmap="jet", 220 | locator=ticker.LogLocator(), 221 | ) 222 | plt.title("Ez") 223 | geom_outline() 224 | fmax = abs(solver.modes[1].Hx).max() 225 | ax = fig.add_subplot(2, 3, 4) 226 | plt.contour( 227 | x, 228 | y, 229 | abs(solver.modes[1].Hx.T), 230 | fmax * levls, 231 | cmap="jet", 232 | locator=ticker.LogLocator(), 233 | ) 234 | plt.title("Hx") 235 | geom_outline() 236 | ax = fig.add_subplot(2, 3, 5) 237 | plt.contour( 238 | x, 239 | y, 240 | abs(solver.modes[1].Hy.T), 241 | fmax * levls, 242 | cmap="jet", 243 | locator=ticker.LogLocator(), 244 | ) 245 | plt.title("Hy") 246 | geom_outline() 247 | ax = fig.add_subplot(2, 3, 6) 248 | plt.contour( 249 | x, 250 | y, 251 | abs(solver.modes[1].Hz.T), 252 | fmax * levls, 253 | cmap="jet", 254 | locator=ticker.LogLocator(), 255 | ) 256 | plt.title("Hz") 257 | geom_outline() 258 | plt.show() 259 | 260 | ExatH = signal.convolve2d(solver.modes[1].Ex, [[0.25, 0.25], [0.25, 0.25]]) 261 | EyatH = signal.convolve2d(solver.modes[1].Ey, [[0.25, 0.25], [0.25, 0.25]]) 262 | EzatH = signal.convolve2d(solver.modes[1].Ez, [[0.25, 0.25], [0.25, 0.25]]) 263 | # Stokes parameters 264 | q1 = ExatH * numpy.conjugate(solver.modes[1].Hy) 265 | q2 = EyatH * numpy.conjugate(solver.modes[1].Hx) 266 | q3 = EyatH * numpy.conjugate(solver.modes[1].Hy) 267 | q4 = ExatH * numpy.conjugate(solver.modes[1].Hx) 268 | 269 | S0 = q1.real - q2.real 270 | S1 = q1.real + q2.real 271 | S2 = q3.real - q4.real 272 | S3 = q3.imag + q4.imag 273 | denom = S0.sum() 274 | print("ave S1=", S1.sum() / denom) 275 | print("ave S2=", S2.sum() / denom) 276 | print("ave S3=", S3.sum() / denom) 277 | 278 | fmax = abs(S0).max() 279 | fig = plt.figure() 280 | ax = fig.add_subplot(2, 2, 1) 281 | plt.contour(x, y, abs(S0.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 282 | plt.title("S0") 283 | geom_outline() 284 | ax = fig.add_subplot(2, 2, 2) 285 | plt.contour(x, y, abs(S1.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 286 | plt.title("S1") 287 | geom_outline() 288 | ax = fig.add_subplot(2, 2, 3) 289 | plt.contour(x, y, abs(S2.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 290 | plt.title("S2") 291 | geom_outline() 292 | ax = fig.add_subplot(2, 2, 4) 293 | plt.contour(x, y, abs(S3.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 294 | plt.title("S3") 295 | geom_outline() 296 | plt.show() 297 | -------------------------------------------------------------------------------- /examples/ex_modesolver_dch_anis.py: -------------------------------------------------------------------------------- 1 | """Fully vectorial finite-difference mode solver example. 2 | 3 | Flexible mode solver example for fundamental modes for anisotropic media 4 | by David Hutchings, School of Engineering, University of Glasgow 5 | David.Hutchings@glasgow.ac.uk 6 | """ 7 | 8 | import numpy 9 | import EMpy 10 | import matplotlib.pyplot as plt 11 | from matplotlib import ticker 12 | from scipy import signal 13 | 14 | """Define cross-section geometry of simulation 15 | each rectangular element tuple contains 16 | ("label",xmin,ymin,xmax,ymax,eps) for isotropic or 17 | ("label",xmin,ymin,xmax,ymax,eps_xx, eps_xy, eps_yx, eps_yy, eps_zz) for anisotropic 18 | first element tuple defines simulation extent 19 | units should match units of wavelength wl 20 | """ 21 | geom = ( 22 | ("base", -0.7, -0.5, 0.7, 0.9, 1.44**2), 23 | ("core", -0.35, 0.0, 0.35, 0.34, 3.48**2), 24 | ("clad", -0.35, 0.34, 0.35, 0.44, 2.19**2, -0.006j, 0.006j, 2.19**2, 2.19**2), 25 | ("slot", -0.05, 0.00, 0.05, 0.34, 2.19**2, -0.006j, 0.006j, 2.19**2, 2.19**2), 26 | ) 27 | # clad corresponds to Ce:TbIG with -3200deg/cm 28 | 29 | wl = 1.55 30 | nx, ny = 141, 141 31 | 32 | 33 | def epsfunc(x_, y_): 34 | """Return a matrix describing a 2d material. 35 | 36 | :param x_: x values 37 | :param y_: y values 38 | :return: 2d-matrix *5 39 | """ 40 | # assume base medium is isotropic 41 | working = numpy.zeros((x_.size, y_.size, 5), dtype="complex") 42 | working[:, :, 0] = geom[0][5] * numpy.ones((x_.size, y_.size)) 43 | working[:, :, 3] = geom[0][5] * numpy.ones((x_.size, y_.size)) 44 | working[:, :, 4] = geom[0][5] * numpy.ones((x_.size, y_.size)) 45 | for i in range(1, len(geom)): 46 | ixmin = numpy.searchsorted(x_, geom[i][1], side="left") 47 | iymin = numpy.searchsorted(y_, geom[i][2], side="left") 48 | ixmax = numpy.searchsorted(x_, geom[i][3], side="right") 49 | iymax = numpy.searchsorted(y_, geom[i][4], side="right") 50 | if len(geom[i]) == 6: 51 | # isotropic 52 | working[ixmin:ixmax, iymin:iymax, :] = [ 53 | geom[i][5], 54 | 0.0, 55 | 0.0, 56 | geom[i][5], 57 | geom[i][5], 58 | ] 59 | else: 60 | # anisotropic 61 | working[ixmin:ixmax, iymin:iymax, :] = [ 62 | geom[i][5], 63 | geom[i][6], 64 | geom[i][7], 65 | geom[i][8], 66 | geom[i][9], 67 | ] 68 | 69 | return working 70 | 71 | 72 | x = numpy.linspace(geom[0][1], geom[0][3], nx) 73 | y = numpy.linspace(geom[0][2], geom[0][4], ny) 74 | 75 | neigs = 2 76 | tol = 1e-6 77 | boundary = "0000" 78 | 79 | solver = EMpy.modesolvers.FD.VFDModeSolver(wl, x, y, epsfunc, boundary).solve( 80 | neigs, tol 81 | ) 82 | 83 | levls = numpy.geomspace(1.0 / 32.0, 1.0, num=11) 84 | levls2 = numpy.geomspace(1.0 / 1024.0, 1.0, num=11) 85 | xe = signal.convolve(x, [0.5, 0.5])[1:-1] 86 | ye = signal.convolve(y, [0.5, 0.5])[1:-1] 87 | 88 | 89 | def geom_outline(): 90 | ax.set_xlim(geom[0][1], geom[0][3]) 91 | ax.set_ylim(geom[0][2], geom[0][4]) 92 | for i in range(1, len(geom)): 93 | plt.hlines(geom[i][2], geom[i][1], geom[i][3]) 94 | plt.hlines(geom[i][4], geom[i][1], geom[i][3]) 95 | plt.vlines(geom[i][1], geom[i][2], geom[i][4]) 96 | plt.vlines(geom[i][3], geom[i][2], geom[i][4]) 97 | return 98 | 99 | 100 | print(solver.modes[0].neff) 101 | fmax = abs(solver.modes[0].Ex).max() 102 | fig = plt.figure() 103 | ax = fig.add_subplot(2, 3, 1) 104 | plt.contour( 105 | xe, 106 | ye, 107 | abs(solver.modes[0].Ex.T), 108 | fmax * levls, 109 | cmap="jet", 110 | locator=ticker.LogLocator(), 111 | ) 112 | plt.title("Ex") 113 | geom_outline() 114 | ax = fig.add_subplot(2, 3, 2) 115 | plt.contour( 116 | xe, 117 | ye, 118 | abs(solver.modes[0].Ey.T), 119 | fmax * levls, 120 | cmap="jet", 121 | locator=ticker.LogLocator(), 122 | ) 123 | plt.title("Ey") 124 | geom_outline() 125 | ax = fig.add_subplot(2, 3, 3) 126 | plt.contour( 127 | xe, 128 | ye, 129 | abs(solver.modes[0].Ez.T), 130 | fmax * levls, 131 | cmap="jet", 132 | locator=ticker.LogLocator(), 133 | ) 134 | plt.title("Ez") 135 | geom_outline() 136 | fmax = abs(solver.modes[0].Hy).max() 137 | ax = fig.add_subplot(2, 3, 4) 138 | plt.contour( 139 | x, 140 | y, 141 | abs(solver.modes[0].Hx.T), 142 | fmax * levls, 143 | cmap="jet", 144 | locator=ticker.LogLocator(), 145 | ) 146 | plt.title("Hx") 147 | geom_outline() 148 | ax = fig.add_subplot(2, 3, 5) 149 | plt.contour( 150 | x, 151 | y, 152 | abs(solver.modes[0].Hy.T), 153 | fmax * levls, 154 | cmap="jet", 155 | locator=ticker.LogLocator(), 156 | ) 157 | plt.title("Hy") 158 | geom_outline() 159 | ax = fig.add_subplot(2, 3, 6) 160 | plt.contour( 161 | x, 162 | y, 163 | abs(solver.modes[0].Hz.T), 164 | fmax * levls, 165 | cmap="jet", 166 | locator=ticker.LogLocator(), 167 | ) 168 | plt.title("Hz") 169 | geom_outline() 170 | plt.show() 171 | 172 | ExatH = signal.convolve2d(solver.modes[0].Ex, [[0.25, 0.25], [0.25, 0.25]]) 173 | EyatH = signal.convolve2d(solver.modes[0].Ey, [[0.25, 0.25], [0.25, 0.25]]) 174 | EzatH = signal.convolve2d(solver.modes[0].Ez, [[0.25, 0.25], [0.25, 0.25]]) 175 | # Stokes parameters 176 | q1 = ExatH * numpy.conjugate(solver.modes[0].Hy) 177 | q2 = EyatH * numpy.conjugate(solver.modes[0].Hx) 178 | q3 = EyatH * numpy.conjugate(solver.modes[0].Hy) 179 | q4 = ExatH * numpy.conjugate(solver.modes[0].Hx) 180 | 181 | S0 = q1.real - q2.real 182 | S1 = q1.real + q2.real 183 | S2 = q3.real - q4.real 184 | S3 = q3.imag + q4.imag 185 | denom = S0.sum() 186 | print("ave S1=", S1.sum() / denom) 187 | print("ave S2=", S2.sum() / denom) 188 | print("ave S3=", S3.sum() / denom) 189 | 190 | fmax = abs(S0).max() 191 | fig = plt.figure() 192 | ax = fig.add_subplot(2, 2, 1) 193 | plt.contour(x, y, abs(S0.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 194 | plt.title("S0") 195 | geom_outline() 196 | ax = fig.add_subplot(2, 2, 2) 197 | plt.contour(x, y, abs(S1.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 198 | plt.title("S1") 199 | geom_outline() 200 | ax = fig.add_subplot(2, 2, 3) 201 | plt.contour(x, y, abs(S2.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 202 | plt.title("S2") 203 | geom_outline() 204 | ax = fig.add_subplot(2, 2, 4) 205 | plt.contour(x, y, abs(S3.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 206 | plt.title("S3") 207 | geom_outline() 208 | plt.show() 209 | 210 | print(solver.modes[1].neff) 211 | fmax = abs(solver.modes[1].Ey).max() 212 | fig = plt.figure() 213 | ax = fig.add_subplot(2, 3, 1) 214 | plt.contour( 215 | xe, 216 | ye, 217 | abs(solver.modes[1].Ex.T), 218 | fmax * levls, 219 | cmap="jet", 220 | locator=ticker.LogLocator(), 221 | ) 222 | plt.title("Ex") 223 | geom_outline() 224 | ax = fig.add_subplot(2, 3, 2) 225 | plt.contour( 226 | xe, 227 | ye, 228 | abs(solver.modes[1].Ey.T), 229 | fmax * levls, 230 | cmap="jet", 231 | locator=ticker.LogLocator(), 232 | ) 233 | plt.title("Ey") 234 | geom_outline() 235 | ax = fig.add_subplot(2, 3, 3) 236 | plt.contour( 237 | xe, 238 | ye, 239 | abs(solver.modes[1].Ez.T), 240 | fmax * levls, 241 | cmap="jet", 242 | locator=ticker.LogLocator(), 243 | ) 244 | plt.title("Ez") 245 | geom_outline() 246 | fmax = abs(solver.modes[1].Hx).max() 247 | ax = fig.add_subplot(2, 3, 4) 248 | plt.contour( 249 | x, 250 | y, 251 | abs(solver.modes[1].Hx.T), 252 | fmax * levls, 253 | cmap="jet", 254 | locator=ticker.LogLocator(), 255 | ) 256 | plt.title("Hx") 257 | geom_outline() 258 | ax = fig.add_subplot(2, 3, 5) 259 | plt.contour( 260 | x, 261 | y, 262 | abs(solver.modes[1].Hy.T), 263 | fmax * levls, 264 | cmap="jet", 265 | locator=ticker.LogLocator(), 266 | ) 267 | plt.title("Hy") 268 | geom_outline() 269 | ax = fig.add_subplot(2, 3, 6) 270 | plt.contour( 271 | x, 272 | y, 273 | abs(solver.modes[1].Hz.T), 274 | fmax * levls, 275 | cmap="jet", 276 | locator=ticker.LogLocator(), 277 | ) 278 | plt.title("Hz") 279 | geom_outline() 280 | plt.show() 281 | 282 | ExatH = signal.convolve2d(solver.modes[1].Ex, [[0.25, 0.25], [0.25, 0.25]]) 283 | EyatH = signal.convolve2d(solver.modes[1].Ey, [[0.25, 0.25], [0.25, 0.25]]) 284 | EzatH = signal.convolve2d(solver.modes[1].Ez, [[0.25, 0.25], [0.25, 0.25]]) 285 | # Stokes parameters 286 | q1 = ExatH * numpy.conjugate(solver.modes[1].Hy) 287 | q2 = EyatH * numpy.conjugate(solver.modes[1].Hx) 288 | q3 = EyatH * numpy.conjugate(solver.modes[1].Hy) 289 | q4 = ExatH * numpy.conjugate(solver.modes[1].Hx) 290 | 291 | S0 = q1.real - q2.real 292 | S1 = q1.real + q2.real 293 | S2 = q3.real - q4.real 294 | S3 = q3.imag + q4.imag 295 | denom = S0.sum() 296 | print("ave S1=", S1.sum() / denom) 297 | print("ave S2=", S2.sum() / denom) 298 | print("ave S3=", S3.sum() / denom) 299 | 300 | fmax = abs(S0).max() 301 | fig = plt.figure() 302 | ax = fig.add_subplot(2, 2, 1) 303 | plt.contour(x, y, abs(S0.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 304 | plt.title("S0") 305 | geom_outline() 306 | ax = fig.add_subplot(2, 2, 2) 307 | plt.contour(x, y, abs(S1.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 308 | plt.title("S1") 309 | geom_outline() 310 | ax = fig.add_subplot(2, 2, 3) 311 | plt.contour(x, y, abs(S2.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 312 | plt.title("S2") 313 | geom_outline() 314 | ax = fig.add_subplot(2, 2, 4) 315 | plt.contour(x, y, abs(S3.T), fmax * levls2, cmap="jet", locator=ticker.LogLocator()) 316 | plt.title("S3") 317 | geom_outline() 318 | plt.show() 319 | -------------------------------------------------------------------------------- /examples/ex_transfer_matrix.py: -------------------------------------------------------------------------------- 1 | """Transfer matrix example. 2 | 3 | Solve for both an isotropic and anisotropic multilayer. 4 | """ 5 | import numpy as np 6 | import pylab 7 | 8 | import EMpy 9 | 10 | n = np.array([1.0, 2.0, 2.3, 4.3, 3.0]) 11 | d = np.array([np.inf, 1e-6, 2.3e-6, 0.1e-6, np.inf]) 12 | 13 | iso_layers = EMpy.utils.Multilayer() 14 | aniso_layers = EMpy.utils.Multilayer() 15 | 16 | for i in range(n.size): 17 | iso_layers.append( 18 | EMpy.utils.Layer( 19 | EMpy.materials.IsotropicMaterial( 20 | "mat", n0=EMpy.materials.RefractiveIndex(n[i]) 21 | ), 22 | d[i], 23 | ) 24 | ) 25 | aniso_layers.append( 26 | EMpy.utils.Layer( 27 | EMpy.materials.AnisotropicMaterial( 28 | "Air", 29 | epsilon_tensor=EMpy.materials.EpsilonTensor( 30 | epsilon_tensor_const=n[i] ** 2 * EMpy.constants.eps0 * np.eye(3) 31 | ), 32 | ), 33 | d[i], 34 | ) 35 | ) 36 | 37 | theta_inc = EMpy.utils.deg2rad(10.0) 38 | theta_inc_x = theta_inc 39 | theta_inc_y = 0.0 40 | wls = np.linspace(1.4e-6, 1.7e-6, 100) 41 | solution_iso = EMpy.transfer_matrix.IsotropicTransferMatrix( 42 | iso_layers, theta_inc 43 | ).solve(wls) 44 | solution_aniso = EMpy.transfer_matrix.AnisotropicTransferMatrix( 45 | aniso_layers, theta_inc_x, theta_inc_y 46 | ).solve(wls) 47 | 48 | pylab.figure() 49 | pylab.plot( 50 | wls, 51 | solution_iso.Rs, 52 | wls, 53 | solution_iso.Ts, 54 | wls, 55 | solution_iso.Rp, 56 | wls, 57 | solution_iso.Tp, 58 | ) 59 | pylab.title("isotropic") 60 | 61 | pylab.figure() 62 | pylab.plot( 63 | wls, 64 | solution_aniso.R[0, 0, :], 65 | wls, 66 | solution_aniso.R[1, 0, :], 67 | wls, 68 | solution_aniso.R[0, 1, :], 69 | wls, 70 | solution_aniso.R[1, 1, :], 71 | wls, 72 | solution_aniso.T[0, 0, :], 73 | wls, 74 | solution_aniso.T[1, 0, :], 75 | wls, 76 | solution_aniso.T[0, 1, :], 77 | wls, 78 | solution_aniso.T[1, 1, :], 79 | ) 80 | pylab.title("anisotropic") 81 | pylab.show() 82 | -------------------------------------------------------------------------------- /examples/ex_transfer_matrix_2.py: -------------------------------------------------------------------------------- 1 | # taken from http://hyperphysics.phy-astr.gsu.edu/hbase/phyopt/antiref.html#c1 2 | 3 | import numpy 4 | import pylab 5 | 6 | import EMpy 7 | 8 | # define multilayer 9 | n = numpy.array([1.0, 1.38, 1.9044]) 10 | d = numpy.array([numpy.inf, 387.5e-9 / 1.38, numpy.inf]) 11 | iso_layers = EMpy.utils.Multilayer() 12 | for i in range(n.size): 13 | n0 = EMpy.materials.RefractiveIndex(n[i]) 14 | iso_layers.append( 15 | EMpy.utils.Layer(EMpy.materials.IsotropicMaterial("mat", n0=n0), d[i]) 16 | ) 17 | 18 | # define incident wave plane 19 | theta_inc = EMpy.utils.deg2rad(10.0) 20 | wls = numpy.linspace(0.85e-6, 2.25e-6, 300) 21 | 22 | # solve 23 | tm = EMpy.transfer_matrix.IsotropicTransferMatrix(iso_layers, theta_inc) 24 | solution_iso = tm.solve(wls) 25 | 26 | # plot 27 | pylab.figure() 28 | pylab.plot( 29 | wls, 30 | 10 * numpy.log10(solution_iso.Rs), 31 | "rx-", 32 | wls, 33 | 10 * numpy.log10(solution_iso.Rp), 34 | "g.-", 35 | ) 36 | pylab.legend(("Rs", "Rp")) 37 | pylab.title("Single Layer Anti-Reflection Coating") 38 | pylab.xlabel("wavelength /m") 39 | pylab.ylabel("Power /dB") 40 | pylab.grid() 41 | pylab.xlim(wls.min(), wls.max()) 42 | pylab.savefig(__file__ + ".png") 43 | pylab.show() 44 | -------------------------------------------------------------------------------- /examples/ex_transfer_matrix_2.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbolla/EMpy/ea1390669c0e866f27f025347e21bdd1e0232e12/examples/ex_transfer_matrix_2.py.png -------------------------------------------------------------------------------- /examples/ex_transfer_matrix_3.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import EMpy 3 | import pylab 4 | 5 | # define the multilayer 6 | epsilon = [ 7 | 1.0**2 * EMpy.constants.eps0 * numpy.eye(3), 8 | EMpy.constants.eps0 * numpy.diag([2.1, 2.0, 1.9]), 9 | 2.3**2 * EMpy.constants.eps0 * numpy.eye(3), 10 | 4.3**2 * EMpy.constants.eps0 * numpy.eye(3), 11 | 3.0**2 * EMpy.constants.eps0 * numpy.eye(3), 12 | ] 13 | 14 | d = numpy.array([numpy.inf, 1e-6, 2.3e-6, 0.1e-6, numpy.inf]) 15 | 16 | aniso_layers = EMpy.utils.Multilayer() 17 | for i in range(len(epsilon)): 18 | eps = EMpy.materials.EpsilonTensor(epsilon[i] * numpy.eye(3)) 19 | mat = EMpy.materials.AnisotropicMaterial("layer_%d" % i, eps) 20 | layer = EMpy.utils.Layer(mat, d[i]) 21 | aniso_layers.append(layer) 22 | 23 | # define the planewave 24 | theta_inc_x = EMpy.utils.deg2rad(0.0) 25 | theta_inc_y = 0.0 26 | wls = numpy.linspace(1.4e-6, 1.7e-6, 100) 27 | 28 | # solve 29 | tm = EMpy.transfer_matrix.AnisotropicTransferMatrix( 30 | aniso_layers, theta_inc_x, theta_inc_y 31 | ) 32 | solution_aniso = tm.solve(wls) 33 | 34 | # plot 35 | pylab.figure() 36 | pylab.plot( 37 | wls, 38 | solution_aniso.R[0, 0, :], 39 | wls, 40 | solution_aniso.R[1, 0, :], 41 | wls, 42 | solution_aniso.R[0, 1, :], 43 | wls, 44 | solution_aniso.R[1, 1, :], 45 | wls, 46 | solution_aniso.T[0, 0, :], 47 | wls, 48 | solution_aniso.T[1, 0, :], 49 | wls, 50 | solution_aniso.T[0, 1, :], 51 | wls, 52 | solution_aniso.T[1, 1, :], 53 | ) 54 | pylab.legend(("Rss", "Rps", "Rsp", "Rpp", "Tss", "Tps", "Tsp", "Tpp")) 55 | pylab.title("Anisotropic Multilayer") 56 | pylab.xlabel("wavelength /m") 57 | pylab.ylabel("Power /dB") 58 | pylab.xlim(wls.min(), wls.max()) 59 | pylab.show() 60 | -------------------------------------------------------------------------------- /examples/lc_data.dat: -------------------------------------------------------------------------------- 1 | 0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5 5.5 6 6.5 7 7.5 8 8.5 9 9.5 10 12.5 15 17.5 20 2 | 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 3 | 86.5 86.445 86.218 85.357 82.228 78.514 74.974 71.644 68.527 65.595 62.821 60.188 57.682 55.298 53.028 50.867 48.81 46.851 44.985 43.207 41.512 34.144 28.316 23.694 20.016 4 | 86.5 86.394 85.954 84.268 78.175 71.212 64.955 59.464 54.678 50.47 46.724 43.353 40.296 37.506 34.95 32.6 30.434 28.431 26.577 24.858 23.26 16.78 12.191 8.912 6.556 5 | 86.5 86.347 85.71 83.245 74.43 64.807 56.652 49.878 44.257 39.524 35.469 31.941 28.838 26.086 23.63 21.43 19.452 17.669 16.059 14.603 13.284 8.308 5.214 3.277 2.058 6 | 86.5 86.306 85.488 82.297 71.046 59.325 49.868 42.327 36.288 31.369 27.285 23.838 20.892 18.352 16.146 14.222 12.539 11.063 9.765 8.624 7.618 4.11 2.22 1.196 0.638 7 | 86.5 86.268 85.287 81.433 68.052 54.704 44.35 36.343 30.112 25.183 21.212 17.96 15.263 13.005 11.101 9.489 8.119 6.952 5.956 5.104 4.376 2.031 0.942 0.433 0.195 8 | 86.5 86.235 85.11 80.663 65.457 50.859 39.878 31.586 25.29 20.444 16.654 13.646 11.229 9.269 7.669 6.355 5.273 4.379 3.639 3.025 2.516 1.003 0.399 0.157 0.06 9 | 86.5 86.207 84.956 79.991 63.258 47.706 36.281 27.819 21.53 16.812 13.229 10.474 8.333 6.654 5.328 4.276 3.437 2.766 2.228 1.796 1.448 0.496 0.169 0.057 0.018 10 | 86.5 86.184 84.827 79.423 61.447 45.175 33.437 24.875 18.631 14.056 10.676 8.158 6.265 4.832 3.739 2.901 2.256 1.757 1.37 1.07 0.836 0.245 0.072 0.02 0.005 11 | 86.5 86.165 84.723 78.964 60.014 43.211 31.254 22.637 16.452 12.012 8.814 6.5 4.815 3.582 2.674 2.003 1.504 1.132 0.853 0.644 0.487 0.122 0.03 0.007 0.002 12 | 86.5 86.15 84.645 78.617 58.949 41.773 29.667 21.023 14.894 10.567 7.516 5.362 3.837 2.755 1.984 1.433 1.038 0.753 0.548 0.399 0.292 0.061 0.013 0.003 0.001 13 | 86.5 86.141 84.593 78.385 58.244 40.829 28.632 19.976 13.89 9.644 6.694 4.65 3.234 2.253 1.572 1.098 0.769 0.539 0.379 0.266 0.188 0.033 0.006 0.001 0 14 | 86.5 86.136 84.566 78.268 57.893 40.362 28.121 19.46 13.398 9.194 6.297 4.308 2.947 2.015 1.379 0.944 0.646 0.443 0.303 0.208 0.143 0.022 0.003 0 0 15 | 86.5 86.136 84.566 78.268 57.893 40.362 28.121 19.46 13.398 9.194 6.297 4.308 2.947 2.015 1.379 0.944 0.646 0.443 0.303 0.208 0.143 0.022 0.003 0 0 16 | 86.5 86.141 84.593 78.385 58.244 40.829 28.632 19.976 13.89 9.644 6.694 4.65 3.234 2.253 1.572 1.098 0.769 0.539 0.379 0.266 0.188 0.033 0.006 0.001 0 17 | 86.5 86.15 84.645 78.617 58.949 41.773 29.667 21.023 14.894 10.567 7.516 5.362 3.837 2.755 1.984 1.433 1.038 0.753 0.548 0.399 0.292 0.061 0.013 0.003 0.001 18 | 86.5 86.165 84.723 78.964 60.014 43.211 31.254 22.637 16.452 12.012 8.814 6.5 4.815 3.582 2.674 2.003 1.504 1.132 0.853 0.644 0.487 0.122 0.03 0.007 0.002 19 | 86.5 86.184 84.827 79.423 61.447 45.175 33.437 24.875 18.631 14.056 10.676 8.158 6.265 4.832 3.739 2.901 2.256 1.757 1.37 1.07 0.836 0.245 0.072 0.02 0.005 20 | 86.5 86.207 84.956 79.991 63.258 47.706 36.281 27.819 21.53 16.812 13.229 10.474 8.333 6.654 5.328 4.276 3.437 2.766 2.228 1.796 1.448 0.496 0.169 0.057 0.018 21 | 86.5 86.235 85.11 80.663 65.457 50.859 39.878 31.586 25.29 20.444 16.654 13.646 11.229 9.269 7.669 6.355 5.273 4.379 3.639 3.025 2.516 1.003 0.399 0.157 0.06 22 | 86.5 86.268 85.287 81.433 68.052 54.704 44.35 36.343 30.112 25.183 21.212 17.96 15.263 13.005 11.101 9.489 8.119 6.952 5.956 5.104 4.376 2.031 0.942 0.433 0.195 23 | 86.5 86.306 85.488 82.297 71.046 59.325 49.868 42.327 36.288 31.369 27.285 23.838 20.892 18.352 16.146 14.222 12.539 11.063 9.765 8.624 7.618 4.11 2.22 1.196 0.638 24 | 86.5 86.347 85.71 83.245 74.43 64.807 56.652 49.878 44.257 39.524 35.469 31.941 28.838 26.086 23.63 21.43 19.452 17.669 16.059 14.603 13.284 8.308 5.214 3.277 2.058 25 | 86.5 86.394 85.954 84.268 78.175 71.212 64.955 59.464 54.678 50.47 46.724 43.353 40.296 37.506 34.95 32.6 30.434 28.431 26.577 24.858 23.26 16.78 12.191 8.912 6.556 26 | 86.5 86.445 86.218 85.357 82.228 78.514 74.974 71.644 68.527 65.595 62.821 60.188 57.682 55.298 53.028 50.867 48.81 46.851 44.985 43.207 41.512 34.144 28.316 23.694 20.016 27 | 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 86.5 28 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | # See https://mypy.readthedocs.io/en/stable/config_file.html 2 | 3 | [mypy] 4 | python_version = 3.10 5 | ignore_missing_imports = True 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements.txt setup.py 6 | # 7 | contourpy==1.3.1 8 | # via matplotlib 9 | cycler==0.12.1 10 | # via matplotlib 11 | fonttools==4.55.3 12 | # via matplotlib 13 | kiwisolver==1.4.8 14 | # via matplotlib 15 | matplotlib==3.10.0 16 | # via ElectroMagneticPython (setup.py) 17 | numpy==2.2.1 18 | # via 19 | # ElectroMagneticPython (setup.py) 20 | # contourpy 21 | # matplotlib 22 | # scipy 23 | packaging==24.2 24 | # via matplotlib 25 | pillow==11.1.0 26 | # via matplotlib 27 | pyparsing==3.2.1 28 | # via matplotlib 29 | python-dateutil==2.9.0.post0 30 | # via matplotlib 31 | scipy==1.15.0 32 | # via ElectroMagneticPython (setup.py) 33 | six==1.17.0 34 | # via python-dateutil 35 | -------------------------------------------------------------------------------- /requirements_dev.in: -------------------------------------------------------------------------------- 1 | black 2 | bump2version 3 | flake8 4 | ipython 5 | mypy 6 | pip-tools>=6.4 7 | pyflakes 8 | pytest 9 | tox>=3.24 10 | 11 | packaging>=20 12 | pyparsing>=3 13 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements_dev.txt requirements_dev.in 6 | # 7 | asttokens==3.0.0 8 | # via stack-data 9 | black==24.10.0 10 | # via -r requirements_dev.in 11 | build==1.2.2.post1 12 | # via pip-tools 13 | bump2version==1.0.1 14 | # via -r requirements_dev.in 15 | cachetools==5.5.0 16 | # via tox 17 | chardet==5.2.0 18 | # via tox 19 | click==8.1.8 20 | # via 21 | # black 22 | # pip-tools 23 | colorama==0.4.6 24 | # via tox 25 | decorator==5.1.1 26 | # via ipython 27 | distlib==0.3.9 28 | # via virtualenv 29 | executing==2.1.0 30 | # via stack-data 31 | filelock==3.16.1 32 | # via 33 | # tox 34 | # virtualenv 35 | flake8==7.1.1 36 | # via -r requirements_dev.in 37 | iniconfig==2.0.0 38 | # via pytest 39 | ipython==8.31.0 40 | # via -r requirements_dev.in 41 | jedi==0.19.2 42 | # via ipython 43 | matplotlib-inline==0.1.7 44 | # via ipython 45 | mccabe==0.7.0 46 | # via flake8 47 | mypy==1.14.1 48 | # via -r requirements_dev.in 49 | mypy-extensions==1.0.0 50 | # via 51 | # black 52 | # mypy 53 | packaging==24.2 54 | # via 55 | # -r requirements_dev.in 56 | # black 57 | # build 58 | # pyproject-api 59 | # pytest 60 | # tox 61 | parso==0.8.4 62 | # via jedi 63 | pathspec==0.12.1 64 | # via black 65 | pexpect==4.9.0 66 | # via ipython 67 | pip-tools==7.4.1 68 | # via -r requirements_dev.in 69 | platformdirs==4.3.6 70 | # via 71 | # black 72 | # tox 73 | # virtualenv 74 | pluggy==1.5.0 75 | # via 76 | # pytest 77 | # tox 78 | prompt-toolkit==3.0.48 79 | # via ipython 80 | ptyprocess==0.7.0 81 | # via pexpect 82 | pure-eval==0.2.3 83 | # via stack-data 84 | pycodestyle==2.12.1 85 | # via flake8 86 | pyflakes==3.2.0 87 | # via 88 | # -r requirements_dev.in 89 | # flake8 90 | pygments==2.19.0 91 | # via ipython 92 | pyparsing==3.2.1 93 | # via -r requirements_dev.in 94 | pyproject-api==1.8.0 95 | # via tox 96 | pyproject-hooks==1.2.0 97 | # via 98 | # build 99 | # pip-tools 100 | pytest==8.3.4 101 | # via -r requirements_dev.in 102 | stack-data==0.6.3 103 | # via ipython 104 | tox==4.23.2 105 | # via -r requirements_dev.in 106 | traitlets==5.14.3 107 | # via 108 | # ipython 109 | # matplotlib-inline 110 | typing-extensions==4.12.2 111 | # via mypy 112 | virtualenv==20.28.1 113 | # via tox 114 | wcwidth==0.2.13 115 | # via prompt-toolkit 116 | wheel==0.45.1 117 | # via pip-tools 118 | 119 | # The following packages are considered to be unsafe in a requirements file: 120 | # pip 121 | # setuptools 122 | -------------------------------------------------------------------------------- /scripts/FDTD.py: -------------------------------------------------------------------------------- 1 | """FDTD Postprocessing. 2 | 3 | """ 4 | import os 5 | import numpy 6 | import EMpy.utils 7 | import pylab 8 | 9 | __author__ = "Lorenzo Bolla" 10 | 11 | 12 | class Input: 13 | """Data structure to handle input files.""" 14 | 15 | def __init__(self, filename): 16 | """Set the input filename.""" 17 | self.filename = filename 18 | 19 | def __str__(self): 20 | """Return a representation of the input file.""" 21 | 22 | dftmon_str = "%g ! #timemonitors \n" % len(self.dftmonitors) 23 | if len(self.dftmonitors) > 0: 24 | dftmon_str += "".join( 25 | [ 26 | "%g %g %g %g %g %g\n%g %g\n" 27 | % ( 28 | dm[0][0], 29 | dm[0][1], 30 | dm[0][2], 31 | dm[0][3], 32 | dm[0][4], 33 | dm[0][5], 34 | dm[1][0], 35 | dm[1][1], 36 | ) 37 | for dm in self.dftmonitors 38 | ] 39 | ) 40 | 41 | timemon_str = "%g ! #timemonitors \n" % len(self.dftmonitors) 42 | if len(timemon_str) > 0: 43 | timemon_str += "%g %g \n %s" % ( 44 | self.timemonitors_time_interval[0], 45 | self.timemonitors_time_interval[1], 46 | "".join( 47 | [ 48 | "%g %g %g ! time_monitor #%d\n" % (s[0], s[1], s[2], iss) 49 | for iss, s in enumerate(self.timemonitors) 50 | ] 51 | ), 52 | ) 53 | 54 | return ( 55 | "%g %g %g %g ! dx dy dz cfl \n" 56 | "%g %g %g %g %g %g %s %g %g ! xmax ymax zmax pmlx pmly pmlz pmltype pmlsmooth pmlref \n" 57 | "%g %g %g %g ! xmax ymax zmax pmlx pmly pmlz \n" 58 | "%g ! output3deps? \n" 59 | "%g ! number diel slices \n" 60 | "%s \n" 61 | "%g ! number field slices \n" 62 | "%s \n" 63 | "%g %g %g ! #dielobjs, index of bg, conductivity of bg \n" 64 | "%s" 65 | "%g ! smoothing method \n" 66 | "%g ! #sources \n" 67 | "%s" 68 | "%g %g %g ! lambdamin, lambdamax, dlambda \n" 69 | "%s" 70 | "%s" 71 | % ( 72 | self.dx, 73 | self.dy, 74 | self.dz, 75 | self.cfl, 76 | self.xmax, 77 | self.ymax, 78 | self.zmax, 79 | self.pmlx, 80 | self.pmly, 81 | self.pmlz, 82 | self.pmltype, 83 | self.pmlsmooth, 84 | self.pmlref, 85 | self.start, 86 | self.end, 87 | self.slides, 88 | self.snapshot, 89 | self.output3deps, 90 | len(self.dielslices), 91 | "\n".join( 92 | [ 93 | "%g %g %g ! dielslice #%d" % (d[0], d[1], d[2], dd) 94 | for (dd, d) in enumerate(self.dielslices) 95 | ] 96 | ), 97 | len(self.fieldslices), 98 | "\n".join( 99 | [ 100 | "%g %g %g ! fieldslice #%d" % (f[0], f[1], f[2], ff) 101 | for (ff, f) in enumerate(self.fieldslices) 102 | ] 103 | ), 104 | len(self.dielobjs), 105 | self.bgrix, 106 | self.bgsigma, 107 | "".join(["%s %s\n" % obj for obj in self.dielobjs]), 108 | self.smoothing_method, 109 | len(self.sources), 110 | "".join(["%s\n%s\n%s\n%s\n" % src for src in self.sources]), 111 | self.lambdamin, 112 | self.lambdamax, 113 | self.dlambda, 114 | dftmon_str, 115 | timemon_str, 116 | ) 117 | ) 118 | 119 | def tofile(self, filename=None): 120 | """Save the input data to the input file.""" 121 | if filename is None: 122 | filename = self.filename 123 | f = open(filename, "w") 124 | f.write(self.__str__()) 125 | f.close() 126 | 127 | 128 | class Param: 129 | """Data structure to handle the param file.""" 130 | 131 | def __str__(self): 132 | """Return a representation of the input file.""" 133 | return ( 134 | "%g\n" 135 | "%g\n" 136 | "%g\n" 137 | "%g\n" 138 | "%g\n" 139 | "%g\n" 140 | "%g\n" 141 | "%g\n" 142 | "%g\n" 143 | "%g\n" 144 | "%g\n" 145 | "%g\n" 146 | "%g\n" 147 | "%g\n" 148 | "%s" 149 | % ( 150 | self.dx, 151 | self.dy, 152 | self.dz, 153 | self.dt, 154 | self.mx, 155 | self.my, 156 | self.mz, 157 | self.pmlx, 158 | self.pmly, 159 | self.pmlz, 160 | self.nflux, 161 | self.ntime, 162 | self.step1, 163 | self.step2, 164 | "\n".join( 165 | [ 166 | "%d\n%d\n%d\n%d\n%d\n%d" 167 | % ( 168 | dm["direction"], 169 | dm["nfreq"], 170 | dm["flxlim"][0], 171 | dm["flxlim"][1], 172 | dm["flxlim"][2], 173 | dm["flxlim"][3], 174 | ) 175 | for dm in self.dftmonitors 176 | ] 177 | ), 178 | ) 179 | ) 180 | 181 | 182 | class Sensor: 183 | """Data structure to handle the FFT sensor's data.""" 184 | 185 | def plot(self, n): 186 | """Plot the sensor's fields.""" 187 | pylab.clf() 188 | pylab.hot() 189 | pylab.subplot(2, 2, 1) 190 | pylab.contour(numpy.abs(self.E1[:, :, n].T), 16) 191 | pylab.axis("image") 192 | pylab.title("E1") 193 | pylab.subplot(2, 2, 2) 194 | pylab.contour(numpy.abs(self.H1[:, :, n].T), 16) 195 | pylab.axis("image") 196 | pylab.title("H1") 197 | pylab.subplot(2, 2, 3) 198 | pylab.contour(numpy.abs(self.E2[:, :, n].T), 16) 199 | pylab.axis("image") 200 | pylab.title("E2") 201 | pylab.subplot(2, 2, 4) 202 | pylab.contour(numpy.abs(self.H2[:, :, n].T), 16) 203 | pylab.axis("image") 204 | pylab.title("H2") 205 | pylab.show() 206 | 207 | def __str__(self): 208 | """Return a representation of the sensor.""" 209 | return "E1\n%s\nH1\n%s\nE2\n%s\nH2\n%s\n" % (self.E1, self.H1, self.E2, self.H2) 210 | 211 | 212 | class TimeSensor: 213 | """Data structure to handle the time sensor's data.""" 214 | 215 | def plot_Ex(self, logplot=False): 216 | self.__plot_field(self.Ex, logplot) 217 | 218 | def plot_Ey(self, logplot=False): 219 | self.__plot_field(self.Ey, logplot) 220 | 221 | def plot_Ez(self, logplot=False): 222 | self.__plot_field(self.Ez, logplot) 223 | 224 | def plot_Hx(self, logplot=False): 225 | self.__plot_field(self.Hx, logplot) 226 | 227 | def plot_Hy(self, logplot=False): 228 | self.__plot_field(self.Hy, logplot) 229 | 230 | def plot_Hz(self, logplot=False): 231 | self.__plot_field(self.Hz, logplot) 232 | 233 | def __plot_field(self, field, logplot=False): 234 | if logplot: 235 | data = 20 * numpy.log10(1e-20 + numpy.abs(field)) 236 | pylab.plot(self.t, data) 237 | else: 238 | data = field 239 | pylab.plot(self.t, data) 240 | pylab.show() 241 | 242 | 243 | class FDTD: 244 | """FDTD. 245 | Data structure to handle an FDTD simulation. It manages an input file, a param file and the sensors' output. 246 | It can run a simulation via a system call. 247 | """ 248 | 249 | def __init__(self): 250 | self.input = None 251 | self.param = None 252 | self.sensors = None 253 | 254 | def fetch_data( 255 | self, 256 | remote_dir_="./", 257 | input_file="inp.txt", 258 | param_file="param", 259 | directory_="./", 260 | ): 261 | remote_dir = fixdir(remote_dir_) 262 | directory = fixdir(directory_) 263 | # input file 264 | os.system( 265 | "scp -C bollalo001@pico:" + remote_dir + "/" + input_file + " " + directory 266 | ) 267 | # param file 268 | os.system( 269 | "scp -C bollalo001@pico:" + remote_dir + "/" + param_file + " " + directory 270 | ) 271 | # fieldslices, flux and time sensors 272 | os.system( 273 | "scp -C bollalo001@pico:" + remote_dir + "/[EHeh]*_*" + " " + directory 274 | ) 275 | # dielslices 276 | os.system("scp -C bollalo001@pico:" + remote_dir + "/diel*" + " " + directory) 277 | 278 | def put_data(self, remote_dir_="./", input_file="inp.txt", directory_="./"): 279 | remote_dir = fixdir(remote_dir_) 280 | directory = fixdir(directory_) 281 | # input file 282 | os.system("scp -C" + directory + input_file + " bollalo001@pico:" + remote_dir) 283 | # .dat modesolver's files 284 | os.system("scp -C" + directory + "*.dat bollalo001@pico:" + remote_dir) 285 | 286 | def load( 287 | self, directory_="./", input_file="inp.txt", param_file="param", remote_dir_="" 288 | ): 289 | """Load input, param and sensors.""" 290 | remote_dir = fixdir(remote_dir_) 291 | directory = fixdir(directory_) 292 | if remote_dir != "": 293 | self.fetch_data(remote_dir, input_file, param_file, directory) 294 | self.load_input_file(directory, input_file) 295 | self.load_param(directory, param_file) 296 | self.load_sensors(directory) 297 | self.load_time_sensors(directory) 298 | 299 | def load_input_file(self, directory_="./", filename="inp.txt"): 300 | """Load input file.""" 301 | directory = fixdir(directory_) 302 | try: 303 | f = open(directory + filename) 304 | except Exception: 305 | print("ERROR: input file") 306 | return 307 | inp = Input(filename) 308 | 309 | (inp.dx, inp.dy, inp.dz, inp.cfl) = numpy.fromstring( 310 | strip_comment(f.readline()), sep=" " 311 | ) 312 | tmp = strip_comment(f.readline()) 313 | tmp_idx = tmp.find("P") 314 | if tmp_idx > 0: 315 | inp.pmltype = "P" 316 | else: 317 | tmp_idx = tmp.find("G") 318 | if tmp_idx > 0: 319 | inp.pmltype = "G" 320 | else: 321 | raise ValueError("wrong pmltype") 322 | (inp.xmax, inp.ymax, inp.zmax, inp.pmlx, inp.pmly, inp.pmlz) = numpy.fromstring( 323 | tmp[:tmp_idx], sep=" " 324 | ) 325 | (inp.pmlsmooth, inp.pmlref) = numpy.fromstring(tmp[tmp_idx + 1 :], sep=" ") 326 | (inp.start, inp.end, inp.slides, inp.snapshot) = numpy.fromstring( 327 | strip_comment(f.readline()), sep=" " 328 | ) 329 | inp.output3deps = numpy.fromstring(strip_comment(f.readline()), sep=" ") 330 | 331 | # dielslices 332 | ndielslices = numpy.fromstring(strip_comment(f.readline()), sep=" ") 333 | inp.dielslices = [] 334 | for i in range(ndielslices): 335 | inp.dielslices.append( 336 | numpy.fromstring(strip_comment(f.readline()), sep=" ") 337 | ) 338 | 339 | # fieldslices 340 | nfieldslices = numpy.fromstring(strip_comment(f.readline()), sep=" ") 341 | inp.fieldslices = [] 342 | for i in range(nfieldslices): 343 | inp.fieldslices.append( 344 | numpy.fromstring(strip_comment(f.readline()), sep=" ") 345 | ) 346 | 347 | # dielobjs 348 | (ndielobjs, inp.bgrix, inp.bgsigma) = numpy.fromstring( 349 | strip_comment(f.readline()), sep=" " 350 | ) 351 | inp.dielobjs = [] 352 | for i in range(int(ndielobjs)): 353 | inp.dielobjs.append( 354 | (strip_comment(f.readline()), strip_comment(f.readline())) 355 | ) 356 | inp.smoothing_method = numpy.fromstring(strip_comment(f.readline()), sep=" ") 357 | 358 | # sources 359 | nsources = numpy.fromstring(strip_comment(f.readline()), dtype=int, sep=" ") 360 | inp.sources = [] 361 | # (inp.time_dependence, inp.wls, inp.pwidth, inp.shift) = numpy.fromstring(strip_comment(f.readline()), sep = ' ') 362 | for i in range(nsources): 363 | inp.sources.append( 364 | ( 365 | strip_comment(f.readline()), 366 | strip_comment(f.readline()), 367 | strip_comment(f.readline()), 368 | strip_comment(f.readline()), 369 | ) 370 | ) 371 | 372 | # dft monitors 373 | (inp.lambdamin, inp.lambdamax, inp.dlambda) = numpy.fromstring( 374 | strip_comment(f.readline()), sep=" " 375 | ) 376 | ndftmonitors = numpy.fromstring(strip_comment(f.readline()), dtype=int, sep=" ") 377 | inp.dftmonitors = [] 378 | for i in range(ndftmonitors): 379 | inp.dftmonitors.append( 380 | ( 381 | numpy.fromstring(strip_comment(f.readline()), sep=" "), 382 | numpy.fromstring(strip_comment(f.readline()), sep=" "), 383 | ) 384 | ) 385 | 386 | # time monitors 387 | ntimemonitors = numpy.fromstring(strip_comment(f.readline()), sep=" ") 388 | inp.timemonitors_time_interval = numpy.fromstring( 389 | strip_comment(f.readline()), sep=" " 390 | ) 391 | inp.timemonitors = [] 392 | for i in range(ntimemonitors): 393 | inp.timemonitors.append( 394 | numpy.fromstring(strip_comment(f.readline()), sep=" ") 395 | ) 396 | 397 | f.close() 398 | self.input = inp 399 | 400 | def load_param(self, directory_="./", filename="param"): 401 | """Load param file.""" 402 | directory = fixdir(directory_) 403 | param = Param() 404 | try: 405 | data = numpy.fromfile(directory + filename, sep=" ") 406 | except Exception: 407 | print("ERROR: param file") 408 | return 409 | param.dx, param.dy, param.dz, param.dt = data[0:4] 410 | ( 411 | param.mx, 412 | param.my, 413 | param.mz, 414 | param.pmlx, 415 | param.pmly, 416 | param.pmlz, 417 | param.nflux, 418 | param.ntime, 419 | param.step1, 420 | param.step2, 421 | ) = data[4:14].astype(numpy.int32) 422 | param.dftmonitors = [] 423 | for iflux in range(int(param.nflux)): 424 | direction, nfreq = data[14 + iflux * 6 : 16 + iflux * 6] 425 | flxlim = data[16 + iflux * 6 : 20 + iflux * 6] 426 | param.dftmonitors.append( 427 | {"direction": int(direction), "nfreq": int(nfreq), "flxlim": flxlim} 428 | ) 429 | self.param = param 430 | 431 | def load_time_sensors(self, directory_="./"): 432 | """Load time sensors.""" 433 | directory = fixdir(directory_) 434 | time_sensors = [] 435 | if self.param is None: 436 | self.load_param(directory) 437 | for itime in range(self.param.ntime): 438 | tmp = TimeSensor() 439 | tmp.Ex = load_fortran_unformatted(directory + "Ex_time_%02d" % (itime + 1)) 440 | tmp.Ey = load_fortran_unformatted(directory + "Ey_time_%02d" % (itime + 1)) 441 | tmp.Ez = load_fortran_unformatted(directory + "Ez_time_%02d" % (itime + 1)) 442 | tmp.Hx = load_fortran_unformatted(directory + "Hx_time_%02d" % (itime + 1)) 443 | tmp.Hy = load_fortran_unformatted(directory + "Hy_time_%02d" % (itime + 1)) 444 | tmp.Hz = load_fortran_unformatted(directory + "Hz_time_%02d" % (itime + 1)) 445 | tmp.t = self.param.dt * numpy.arange(len(tmp.Ex)) 446 | time_sensors.append(tmp) 447 | 448 | self.time_sensors = time_sensors 449 | 450 | def load_sensors(self, directory_="./"): 451 | """Load sensors.""" 452 | directory = fixdir(directory_) 453 | sensors = [] 454 | if self.param is None: 455 | self.load_param(directory) 456 | for iflux in range(self.param.nflux): 457 | tmp = Sensor() 458 | dm = self.param.dftmonitors[iflux] 459 | tmp.E1 = load_fortran_unformatted(directory + "E1_%02d" % (iflux + 1)) 460 | tmp.H1 = load_fortran_unformatted(directory + "H1_%02d" % (iflux + 1)) 461 | tmp.E2 = load_fortran_unformatted(directory + "E2_%02d" % (iflux + 1)) 462 | tmp.H2 = load_fortran_unformatted(directory + "H2_%02d" % (iflux + 1)) 463 | # [tmp.E1, tmp.H1, tmp.E2, tmp.H2] = map(lambda x: x[0::2] + 1j * x[1::2], [tmp.E1, tmp.H1, tmp.E2, tmp.H2]) 464 | # more memory efficient! 465 | tmp.E1 = tmp.E1[0::2] + 1j * tmp.E1[1::2] 466 | tmp.H1 = tmp.H1[0::2] + 1j * tmp.H1[1::2] 467 | tmp.E2 = tmp.E2[0::2] + 1j * tmp.E2[1::2] 468 | tmp.H2 = tmp.H2[0::2] + 1j * tmp.H2[1::2] 469 | 470 | n1 = dm["flxlim"][1] - dm["flxlim"][0] + 1 471 | n2 = dm["flxlim"][3] - dm["flxlim"][2] + 1 472 | tmp.E1 = tmp.E1.reshape((n1, n2 + 1, dm["nfreq"]), order="F") 473 | tmp.H1 = tmp.H1.reshape((n1, n2 + 1, dm["nfreq"]), order="F") 474 | tmp.E2 = tmp.E2.reshape((n1 + 1, n2, dm["nfreq"]), order="F") 475 | tmp.H2 = tmp.H2.reshape((n1 + 1, n2, dm["nfreq"]), order="F") 476 | if dm["direction"] == 1: 477 | # sensors in the x-direction 478 | tmp.dx1 = self.param.dy 479 | tmp.dx2 = self.param.dz 480 | elif dm["direction"] == 2: 481 | # sensors in the y-direction 482 | tmp.dx1 = self.param.dx 483 | tmp.dx2 = self.param.dz 484 | elif dm["direction"] == 3: 485 | # sensors in the z-direction 486 | tmp.dx1 = self.param.dx 487 | tmp.dx2 = self.param.dy 488 | else: 489 | raise ValueError("wrong direction") 490 | 491 | sensors.append(tmp) 492 | 493 | self.sensors = sensors 494 | 495 | def viz2D(self, filename, directory_="./", const_dir="z", logplot=False): 496 | """Visualize a slice.""" 497 | directory = fixdir(directory_) 498 | data = load_fortran_unformatted(directory + filename) 499 | if self.param is None: 500 | self.load_param(directory) 501 | x = numpy.linspace( 502 | self.param.dx / 2.0, 503 | self.param.dx * self.param.mx - self.param.dx / 2.0, 504 | self.param.mx, 505 | ) 506 | y = numpy.linspace( 507 | self.param.dy / 2.0, 508 | self.param.dy * self.param.my - self.param.dy / 2.0, 509 | self.param.my, 510 | ) 511 | z = numpy.linspace( 512 | self.param.dz / 2.0, 513 | self.param.dz * self.param.mz - self.param.dz / 2.0, 514 | self.param.mz, 515 | ) 516 | if const_dir == "x": 517 | n1 = self.param.my 518 | n2 = self.param.mz 519 | x1 = y 520 | x2 = z 521 | x1label = "y" 522 | x2label = "z" 523 | elif const_dir == "y": 524 | n1 = self.param.mx 525 | n2 = self.param.mz 526 | x1 = x 527 | x2 = z 528 | x1label = "x" 529 | x2label = "z" 530 | else: 531 | n1 = self.param.mx 532 | n2 = self.param.my 533 | x1 = x 534 | x2 = y 535 | x1label = "x" 536 | x2label = "y" 537 | data = data.reshape((n2, n1)) 538 | pylab.clf() 539 | if logplot: 540 | data = 20 * numpy.log10(numpy.abs(data).clip(1e-30, 1e30)) 541 | pylab.jet() 542 | else: 543 | pylab.hot() 544 | pylab.contour(x1, x2, data, 64) 545 | pylab.colorbar() 546 | pylab.axis("image") 547 | pylab.xlabel(x1label + " /um") 548 | pylab.ylabel(x2label + " /um") 549 | pylab.show() 550 | 551 | def memory(self): 552 | """Estimate the memory occupation.""" 553 | # size_of_char = 1 554 | # size_of_int = 4 555 | size_of_real = 4 556 | # size_of_complex = 2 * size_of_real 557 | # size_of_dielobj = size_of_int + 31 * size_of_real + 2 * 16 * size_of_char 558 | # size_of_source = 9 * size_of_int + 5 * size_of_real + 6 * 16 * size_of_char 559 | # size_of_monitor = (6 + 2) * 6 * size_of_int 560 | 561 | Gb = 1024**3 562 | max_available_RAM = 32 * Gb 563 | 564 | dynamic_alloc_memory = 0 565 | 566 | # epsx, epsy, epsz 567 | dynamic_alloc_memory = ( 568 | dynamic_alloc_memory 569 | + 3 570 | * (self.param.mx + 2 * self.input.pmlx) 571 | * (self.param.my + 2 * self.input.pmly) 572 | * (self.param.mz + 2 * self.input.pmlz) 573 | * size_of_real 574 | ) 575 | # sigma 576 | dynamic_alloc_memory = ( 577 | dynamic_alloc_memory 578 | + 2 579 | * (self.param.mx + 2 * self.input.pmlx) 580 | * (self.param.my + 2 * self.input.pmly) 581 | * (self.param.mz + 2 * self.input.pmlz) 582 | * size_of_real 583 | ) 584 | # cex, cmx 585 | dynamic_alloc_memory = ( 586 | dynamic_alloc_memory 587 | + 2 * 2 * (self.param.mx + 2 * self.input.pmlx) * size_of_real 588 | ) 589 | # cey, cmy 590 | dynamic_alloc_memory = ( 591 | dynamic_alloc_memory 592 | + 2 * 2 * (self.param.my + 2 * self.input.pmly) * size_of_real 593 | ) 594 | # cez, cmz 595 | dynamic_alloc_memory = ( 596 | dynamic_alloc_memory 597 | + 2 * 2 * (self.param.mz + 2 * self.input.pmlz) * size_of_real 598 | ) 599 | 600 | # exy, exz, eyx, eyz, ... 601 | dynamic_alloc_memory = ( 602 | dynamic_alloc_memory 603 | + 12 604 | * (self.param.mx + 2 * self.input.pmlx) 605 | * (self.param.my + 2 * self.input.pmly) 606 | * (self.param.mz + 2 * self.input.pmlz) 607 | * size_of_real 608 | ) 609 | 610 | print( 611 | "Alloc mem = %g Gb, [%d%%]" 612 | % ( 613 | 1.0 * dynamic_alloc_memory / Gb, 614 | int(1.0 * dynamic_alloc_memory / max_available_RAM * 100), 615 | ) 616 | ) 617 | 618 | def run( 619 | self, 620 | directory_="./", 621 | exe_file="/xlv1/labsoi_devices/devices/f3d", 622 | output_file="output", 623 | ncpu=12, 624 | bg=False, 625 | remote=True, 626 | ): 627 | """Run the simulation, possibly in remote.""" 628 | directory = fixdir(directory_) 629 | # os.environ['OMP_NUM_THREAD'] = str(ncpu) 630 | # cmd = 'dplace -x6 ' + exe_file + ' > ' + output_file 631 | cmd = ( 632 | "cd" 633 | + directory 634 | + "; setenv OMP_NUM_THREAD" 635 | + str(ncpu) 636 | + "dplace -x6 " 637 | + exe_file 638 | + " > " 639 | + output_file 640 | ) 641 | if bg: 642 | cmd += "&" 643 | if remote: 644 | cmd = 'ssh pico "' + cmd + '"' 645 | os.system(cmd) 646 | 647 | def __str__(self): 648 | """Return a representation of the FDTD data structure.""" 649 | return "INPUT\n%s\nPARAM\n%s\nSENSORS\n%s\n" % ( 650 | self.input, 651 | self.param, 652 | self.sensors, 653 | ) 654 | 655 | 656 | def load_fortran_unformatted(filename): 657 | """Load data from an unformatted fortran binary file.""" 658 | try: 659 | f = open(filename, "rb") 660 | except Exception: 661 | print("ERROR") 662 | return 663 | nbytes = numpy.fromfile(f, dtype=numpy.int32, count=1) 664 | n = nbytes / numpy.float32().nbytes 665 | data = numpy.fromfile(f, dtype=numpy.float32, count=n) 666 | f.close() 667 | return data 668 | 669 | 670 | def strip_comment(line): 671 | """Get rid of fortran comments.""" 672 | idx = line.find("!") 673 | if idx != -1: 674 | return line[:idx].strip() 675 | return line 676 | 677 | 678 | def fixdir(str, sep="/"): 679 | tmp = str 680 | if len(tmp) > 0: 681 | if tmp[-1] != sep: 682 | tmp += sep 683 | return tmp 684 | 685 | 686 | # def overlap_f(simul, solver, nwl): 687 | # vu = numpy.zeros((len(simul.sensors), len(solver.modes)), dtype=complex) 688 | # ju = numpy.zeros((len(simul.sensors), len(solver.modes)), dtype=complex) 689 | # for isens, sens in enumerate(simul.sensors): 690 | # for imode, mode in enumerate(solver.modes): 691 | # Ex, Ey, Ez, Hx, Hy, Hz = mode.get_fields_for_FDTD() 692 | # vu[isens, imode] = 0.5 * ( 693 | # numpy.trapz(numpy.trapz(sens.E1[:,1:-1,nwl] * Hy, dx=sens.dx2*1e-6), dx=sens.dx1*1e-6) - 694 | # numpy.trapz(numpy.trapz(sens.E2[1:-1,:,nwl] * Hx, dx=sens.dx2*1e-6), dx=sens.dx1*1e-6)) 695 | # ju[isens, imode] = 0.5 * ( 696 | # numpy.trapz(numpy.trapz(sens.H2[1:-1,:,nwl] * Ey, dx=sens.dx2*1e-6), dx=sens.dx1*1e-6) - 697 | # numpy.trapz(numpy.trapz(sens.H1[:,1:-1,nwl] * Ex, dx=sens.dx2*1e-6), dx=sens.dx1*1e-6)) 698 | # A = (vu + ju) / 2. 699 | # B = (vu - ju) / 2. 700 | # Pm = numpy.abs(A)**2 - numpy.abs(B)**2 701 | # P = Pm.sum(axis=1) 702 | # return (vu, ju, A, B, Pm, P) 703 | 704 | 705 | def overlap_f(sensors, solver, nwl): 706 | vu = numpy.zeros((len(sensors), len(solver.modes)), dtype=complex) 707 | ju = numpy.zeros((len(sensors), len(solver.modes)), dtype=complex) 708 | for isens, sens in enumerate(sensors): 709 | x = sens.dx1 * numpy.arange(sens.E2.shape[0]) 710 | y = sens.dx2 * numpy.arange(sens.E1.shape[1]) 711 | for imode, mode in enumerate(solver.modes): 712 | # resample the mode to the sensor's grid 713 | Ex, Ey, Ez, Hx, Hy, Hz = mode.get_fields_for_FDTD(x, y) 714 | # vu[isens, imode] = 0.5 * ( 715 | # numpy.trapz(numpy.trapz(sens.E1[:,1:-1,nwl] * Hy, dx=sens.dx2*1e-6), dx=sens.dx1*1e-6) - 716 | # numpy.trapz(numpy.trapz(sens.E2[1:-1,:,nwl] * Hx, dx=sens.dx2*1e-6), dx=sens.dx1*1e-6)) 717 | # ju[isens, imode] = 0.5 * ( 718 | # numpy.trapz(numpy.trapz(sens.H2[1:-1,:,nwl] * Ey, dx=sens.dx2*1e-6), dx=sens.dx1*1e-6) - 719 | # numpy.trapz(numpy.trapz(sens.H1[:,1:-1,nwl] * Ex, dx=sens.dx2*1e-6), dx=sens.dx1*1e-6)) 720 | vu[isens, imode] = 0.5 * ( 721 | EMpy.utils.trapz2( 722 | sens.E1[:, 1:-1, nwl] * Hy, dx=sens.dx1 * 1e-6, dy=sens.dx2 * 1e-6 723 | ) 724 | - EMpy.utils.trapz2( 725 | sens.E2[1:-1, :, nwl] * Hx, dx=sens.dx1 * 1e-6, dy=sens.dx2 * 1e-6 726 | ) 727 | ) 728 | ju[isens, imode] = 0.5 * ( 729 | EMpy.utils.trapz2( 730 | sens.H2[1:-1, :, nwl] * Ey, dx=sens.dx1 * 1e-6, dy=sens.dx1 * 1e-6 731 | ) 732 | - EMpy.utils.trapz2( 733 | sens.H1[:, 1:-1, nwl] * Ex, dx=sens.dx1 * 1e-6, dy=sens.dx1 * 1e-6 734 | ) 735 | ) 736 | A = (vu + ju) / 2.0 737 | B = (vu - ju) / 2.0 738 | Pm = numpy.abs(A) ** 2 - numpy.abs(B) ** 2 739 | P = Pm.sum(axis=1) 740 | return (vu, ju, A, B, Pm, P) 741 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = sdist upload 3 | 4 | [flake8] 5 | max-line-length = 130 6 | ignore = E203 E741 W503 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | try: 4 | __version__ = open("EMpy/version.py").read().split('"')[1] 5 | except ImportError: 6 | __version__ = "" 7 | 8 | __author__ = "Lorenzo Bolla" 9 | 10 | with open("README.rst", "r") as readme: 11 | long_description = readme.read() 12 | 13 | setup( 14 | name="ElectroMagneticPython", 15 | version=__version__, 16 | author="Lorenzo Bolla", 17 | author_email="code@lbolla.info", 18 | description="EMpy - ElectroMagnetic Python", 19 | long_description=long_description, 20 | url="http://lbolla.github.io/EMpy/", 21 | download_url="https://github.com/lbolla/EMpy", 22 | license="BSD", 23 | platforms=["Windows", "Linux", "Mac OS-X"], 24 | packages=find_packages(), 25 | install_requires=[ 26 | "numpy>=1.18", 27 | "scipy>=1.7", 28 | "matplotlib>=3.1", 29 | ], 30 | provides=["EMpy"], 31 | test_suite="tests", 32 | classifiers=[ 33 | "Development Status :: 5 - Production/Stable", 34 | "Environment :: Console", 35 | "Intended Audience :: Science/Research", 36 | "License :: OSI Approved :: BSD License", 37 | "Natural Language :: English", 38 | "Programming Language :: Python :: 3", 39 | "Operating System :: OS Independent", 40 | "Topic :: Scientific/Engineering :: Physics", 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "Lorenzo Bolla" 2 | -------------------------------------------------------------------------------- /tests/materials_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=no-self-use 2 | from unittest import TestCase 3 | 4 | from numpy import array 5 | from numpy.testing import assert_almost_equal, assert_raises 6 | 7 | import EMpy.materials as mat 8 | 9 | 10 | class RefractiveIndexTest(TestCase): 11 | def test_all_nones(self): 12 | with assert_raises(ValueError): 13 | mat.RefractiveIndex() 14 | 15 | def test_const(self): 16 | test_rix = 1.50 17 | a = mat.RefractiveIndex(n0_const=test_rix) 18 | self.assertEqual(a.get_rix(1.0)[0], array([test_rix])) 19 | 20 | def test_poly(self): 21 | test_poly = [1, 1] # n(wl) = 1 * wl + 1 22 | test_rix = 2.0 # n(1) = 1 * 1 + 1 = 2 23 | a = mat.RefractiveIndex(n0_poly=test_poly) 24 | assert_almost_equal(a.get_rix(1.0)[0], array([test_rix])) 25 | 26 | def test_smcoeffs(self): 27 | test_poly = [1] * 6 28 | """ 6-coeffs: 29 | n(wls) = 1. + 30 | B1 * wls ** 2 / (wls ** 2 - C1) + 31 | B2 * wls ** 2 / (wls ** 2 - C2) + 32 | B3 * wls ** 2 / (wls ** 2 - C3) 33 | """ 34 | test_rix = 1.0536712127723509e-08 35 | a = mat.RefractiveIndex(n0_smcoeffs=test_poly) 36 | assert_almost_equal(a.get_rix(0.5)[0], array([test_rix])) 37 | 38 | def test_func(self): 39 | test_rix = 1.50 40 | 41 | def test_func_const(x): 42 | # returns a const 43 | return 0.0 * x + test_rix 44 | 45 | a = mat.RefractiveIndex(n0_func=test_func_const) 46 | assert_almost_equal(a.get_rix([1.0, 1.5]), array([1.5, 1.5])) 47 | 48 | def test_func_var(x): 49 | # returns a const 50 | return 1.0 * x + test_rix 51 | 52 | b = mat.RefractiveIndex(n0_func=test_func_var) 53 | assert_almost_equal(b.get_rix([1.0, 1.5]), array([2.5, 3.0])) 54 | 55 | def test_known(self): 56 | test_rix = 1.50 57 | test_wl = 1.0 58 | n0_known = {test_wl: test_rix} 59 | a = mat.RefractiveIndex(n0_known=n0_known) 60 | self.assertEqual(a.get_rix(test_wl)[0], array([test_rix])) 61 | -------------------------------------------------------------------------------- /tests/utils_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=no-self-use 2 | from unittest import TestCase 3 | 4 | import random 5 | import math 6 | 7 | from numpy import pi 8 | from numpy.testing import assert_almost_equal 9 | 10 | import EMpy.utils as U 11 | 12 | 13 | class UtilsTest(TestCase): 14 | def test_deg2rad(self): 15 | # TODO use hypothesis 16 | x = -2 * pi + 4 * pi * random.random() 17 | assert_almost_equal(x, U.deg2rad(U.rad2deg(x))) 18 | 19 | def test_norm(self): 20 | self.assertEqual(U.norm([1, 0, 0]), 1) 21 | self.assertEqual(U.norm([0, 1, 0]), 1) 22 | self.assertEqual(U.norm([0, 0, 1]), 1) 23 | assert_almost_equal(U.norm([1, 0, 1]), math.sqrt(2)) 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py312, py311, py310 8 | 9 | [testenv] 10 | deps = pytest -rrequirements.txt 11 | commands = pytest 12 | --------------------------------------------------------------------------------