├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ ├── benchmark.yml │ ├── build-docker.yml │ ├── linting.yml │ ├── pytest.yml │ ├── python-publish.yml │ └── requirements.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .pylintrc ├── .readthedocs.yml ├── .vscode ├── ltex.dictionary.en-US.txt └── settings.json ├── .zenodo.json ├── AUTHORS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── _static │ ├── basic_usage.png │ ├── custom_fitting.png │ ├── mueller_matrix.png │ ├── multilayer.png │ └── style.css ├── authors.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── database.rst ├── dispersions.rst ├── experiment.rst ├── helpers.rst ├── index.rst ├── installation.rst ├── kkr.rst ├── license.rst ├── materials.rst ├── modules.rst ├── plot.rst ├── read_write.rst ├── requirements.txt ├── result.rst ├── solvers.rst └── structure.rst ├── examples ├── Basic Usage │ ├── Basic Usage.ipynb │ ├── SiO2_diel_func.csv │ ├── SiO2onSi.ellips.nxs │ └── Si_Aspnes.mat ├── Bragg-mirror │ ├── Bragg-Mirror.ipynb │ └── validation-Bragg.ipynb ├── Custom fitting │ ├── Custom fitting example.ipynb │ └── SiO2onSi.ellips.nxs ├── Ellipsometry uniaxial materials │ ├── Fujiwara-641.ipynb │ ├── Fujiwara-641.py │ ├── Fujiwara-642.ipynb │ └── Fujiwara-642.py ├── Interfaces │ ├── Interferences.ipynb │ └── interface-reflection.ipynb ├── Liquid crystals │ ├── cholesteric-liquid.ipynb │ ├── twisted-nematic.ipynb │ └── validation-cholesteric.ipynb ├── SiO2_Si Mueller Matrix │ ├── SiO2_Si Mueller Matrix.ipynb │ └── Wafer_MM_70.txt ├── TiO2 Fit │ ├── TiO2 Multilayerfit.ipynb │ └── TiO2_400cycles.txt ├── Total internal reflection │ ├── FrustratedTIR-angle.ipynb │ ├── FrustratedTIR-thickness.ipynb │ └── TIR.ipynb └── gallery │ ├── README.rst │ ├── SiO2_diel_func.csv │ ├── SiO2onSi.ellips.nxs │ ├── TiO2_400cycles.txt │ ├── Wafer_MM_70.txt │ ├── plot_01_basic_usage.py │ ├── plot_02_TiO2_multilayer.py │ ├── plot_03_custom_fitting.py │ ├── plot_SiO2_Si_MM.py │ ├── plot_bragg_mirror.py │ ├── plot_cholesteric_lq.py │ └── plot_interface_reflection.py ├── logo ├── logo_bw.svg ├── logo_dark.svg ├── logo_gray.svg └── logo_light.svg ├── pyproject.toml ├── requirements ├── dev-requirements.txt ├── fitting-requirements.txt └── requirements.txt ├── scripts ├── generate_ipynb_from_gallery.sh └── generate_requirements.sh ├── src └── elli │ ├── __init__.py │ ├── database │ ├── __init__.py │ ├── materials_db.py │ └── refractive_index_info.py │ ├── dispersions │ ├── __init__.py │ ├── base_dispersion.py │ ├── cauchy.py │ ├── cauchy_custom.py │ ├── cauchy_urbach.py │ ├── cody_lorentz.py │ ├── constant_refractive_index.py │ ├── drude_energy.py │ ├── drude_resistivity.py │ ├── epsilon_inf.py │ ├── formula.py │ ├── gaussian.py │ ├── lorentz_energy.py │ ├── lorentz_lambda.py │ ├── poles.py │ ├── polynomial.py │ ├── pseudo_dielectric.py │ ├── sellmeier.py │ ├── sellmeier_custom.py │ ├── table_epsilon.py │ ├── table_index.py │ ├── table_spectraray.py │ ├── tanguy.py │ └── tauc_lorentz.py │ ├── experiment.py │ ├── fitting │ ├── __init__.py │ ├── decorator.py │ ├── decorator_mmatrix.py │ ├── decorator_psi_delta.py │ └── params_hist.py │ ├── formula_parser │ ├── __init__.py │ ├── dispersion_function_grammar.lark │ └── parser.py │ ├── importer │ ├── __init__.py │ ├── accurion.py │ ├── nexus.py │ ├── spectraray.py │ └── woollam.py │ ├── kkr │ ├── __init__.py │ └── kkr.py │ ├── materials.py │ ├── plot │ ├── __init__.py │ ├── mueller_matrix.py │ └── structure.py │ ├── result.py │ ├── solver.py │ ├── solver2x2.py │ ├── solver4x4.py │ ├── structure.py │ ├── units.py │ └── utils.py └── tests ├── benchmark_formula_dispersion.py ├── benchmark_propagators_TiO2.py ├── create_dispersion_prototypes.py ├── fixtures.py ├── test_TiO2.py ├── test_TiO2 ├── Si_Aspnes.mat └── TiO2_400cycles.txt ├── test_accurion.py ├── test_accurion └── Si3N4_on_4inBF33_W02_20240903-150451.ds.dat ├── test_bragg_mirror.py ├── test_dispersion_adding.py ├── test_dispersion_factory.py ├── test_dispersions.py ├── test_dispersions ├── CauchyUrbach_custom_values.csv ├── CauchyUrbach_default.csv ├── Cauchy_custom_values.csv ├── Cauchy_default.csv ├── DrudeEnergy_custom_values.csv ├── DrudeEnergy_default.csv ├── DrudeResistivity_custom_values.csv ├── DrudeResistivity_default.csv ├── Gaussian_custom_values.csv ├── Gaussian_default.csv ├── LorentzEnergy_custom_values.csv ├── LorentzEnergy_default.csv ├── LorentzLambda_custom_values.csv ├── LorentzLambda_default.csv ├── Poles_custom_values.csv ├── Poles_default.csv ├── PseudoDielectricFunction_custom_values.csv ├── Sellmeier_default.csv ├── TableEpsilon_custom_values.csv ├── Table_custom_values.csv ├── Tanguy_custom_values.csv ├── Tanguy_default.csv ├── TaucLorentz_custom_values.csv └── TaucLorentz_default.csv ├── test_formula_dispersion.py ├── test_internal_reflection.py ├── test_kkr.py ├── test_liquid_crystals.py ├── test_materials.py ├── test_mixture_models.py ├── test_nexus.py ├── test_nexus ├── ellips.test.nxs └── ellips_nx_opt.test.nxs ├── test_nexus_formula.py ├── test_nexus_formula ├── MoSe2-Munkhbat-o.nxs ├── MoTe2-Beal.nxs ├── Si-Chandler-Horowitz.nxs ├── Si-Salzberg.nxs ├── WTe2-Munkhbat-alpha.nxs └── YVO4-Shi-e-20C.nxs ├── test_notebook.ipynb ├── test_refractive_index_info.py ├── test_result.py ├── test_solvers.py ├── test_spectraray.py ├── test_spectraray └── Si_SiO2_theta_50_60_70.txt ├── test_structures.py ├── test_uniaxial_crystals.py ├── test_utils.py ├── test_wollam.py └── test_wollam ├── complete_ease_example.dat ├── wvase_example.dat └── wvase_trig.dat /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | source = elli 5 | # omit = bad_file.py 6 | 7 | [paths] 8 | source = 9 | src/ 10 | */site-packages/ 11 | 12 | [report] 13 | # Regexes for lines to exclude from consideration 14 | exclude_lines = 15 | # Have to re-enable the standard pragma 16 | pragma: no cover 17 | 18 | # Don't complain about missing debug-only code: 19 | def __repr__ 20 | if self\.debug 21 | 22 | # Don't complain if tests don't hit defensive assertion code: 23 | raise AssertionError 24 | raise NotImplementedError 25 | 26 | # Don't complain if non-runnable code isn't run: 27 | if 0: 28 | if __name__ == .__main__.: 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every week 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | 8 | permissions: 9 | contents: write 10 | deployments: write 11 | 12 | jobs: 13 | benchmark: 14 | name: Run pytest-benchmark benchmark example 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | - uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.13" 23 | - name: Install deps 24 | run: | 25 | curl -LsSf https://astral.sh/uv/install.sh | sh 26 | uv pip install --system -r requirements/dev-requirements.txt 27 | uv pip install --system torch --index-url https://download.pytorch.org/whl/cpu 28 | - name: Install module 29 | run: | 30 | uv pip install --system . 31 | - name: Run benchmark 32 | run: | 33 | pytest tests/benchmark*.py --benchmark-json output.json 34 | - name: Store benchmark result 35 | if: github.ref == 'refs/heads/master' 36 | uses: benchmark-action/github-action-benchmark@v1 37 | with: 38 | name: Python Benchmark with pytest-benchmark 39 | tool: "pytest" 40 | output-file-path: output.json 41 | # Use personal access token instead of GITHUB_TOKEN due to https://github.community/t/github-action-not-triggering-gh-pages-upon-push/16096 42 | github-token: ${{ secrets.GITHUB_TOKEN }} 43 | auto-push: true 44 | # Show alert with commit comment on detecting possible performance regression 45 | alert-threshold: "200%" 46 | comment-on-alert: false 47 | fail-on-alert: false 48 | - name: Check benchmark threshold 49 | if: github.ref != 'refs/heads/master' 50 | uses: benchmark-action/github-action-benchmark@v1 51 | with: 52 | name: Python Benchmark with pytest-benchmark 53 | tool: "pytest" 54 | output-file-path: output.json 55 | github-token: ${{ secrets.GITHUB_TOKEN }} 56 | auto-push: false 57 | # Show alert with commit comment on detecting possible performance regression 58 | alert-threshold: "200%" 59 | comment-on-alert: true 60 | fail-on-alert: false 61 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | push_to_registry: 9 | name: Push Docker image to Docker Hub 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out the repo 13 | uses: actions/checkout@v4 14 | with: 15 | submodules: recursive 16 | - name: Log in to Docker Hub 17 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | 22 | - name: Extract metadata (tags, labels) for Docker 23 | id: meta 24 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 25 | with: 26 | images: domna/pyelli 27 | 28 | - name: Build and push Docker image 29 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 30 | with: 31 | context: . 32 | push: true 33 | tags: ${{ steps.meta.outputs.tags }} 34 | labels: ${{ steps.meta.outputs.labels }} 35 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | linting: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.13" 17 | - name: Install deps 18 | run: | 19 | pip install sphinx-lint ruff==0.8.5 20 | - name: ruff 21 | run: | 22 | ruff check src/elli tests 23 | - name: ruff formatting 24 | run: | 25 | ruff format --check src/elli tests 26 | - name: Run Sphinx Linting 27 | run: | 28 | sphinx-lint . 29 | -------------------------------------------------------------------------------- /.github/workflows/pytest.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: Pytest 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | test_python: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 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 module 29 | run: | 30 | curl -LsSf https://astral.sh/uv/install.sh | sh 31 | uv pip install --system ".[fitting,dev]" 32 | uv pip install --system torch --index-url https://download.pytorch.org/whl/cpu 33 | - name: Test with pytest 34 | run: | 35 | pytest --nbmake 36 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 📦 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build distribution 📦 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | submodules: recursive 14 | - name: Set up Python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.x" 18 | - name: Install pypa/build 19 | run: >- 20 | python3 -m 21 | pip install 22 | build 23 | --user 24 | - name: Build a binary wheel and a source tarball 25 | run: python3 -m build 26 | - name: Store the distribution packages 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: python-package-distributions 30 | path: dist/ 31 | 32 | publish-to-pypi: 33 | name: >- 34 | Publish to PyPi 35 | if: startsWith(github.ref, 'refs/tags/') 36 | needs: 37 | - build 38 | runs-on: ubuntu-latest 39 | environment: 40 | name: pypi 41 | url: https://pypi.org/p/pyElli/ 42 | permissions: 43 | id-token: write 44 | 45 | steps: 46 | - name: Download all the dists 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: python-package-distributions 50 | path: dist/ 51 | - name: Publish distribution 📦 to PyPI 52 | uses: pypa/gh-action-pypi-publish@release/v1 53 | 54 | github-release: 55 | name: >- 56 | GitHub Release 57 | needs: 58 | - publish-to-pypi 59 | runs-on: ubuntu-latest 60 | 61 | permissions: 62 | contents: write 63 | id-token: write 64 | 65 | steps: 66 | - name: Download all the dists 67 | uses: actions/download-artifact@v4 68 | with: 69 | name: python-package-distributions 70 | path: dist/ 71 | - name: Sign the dists with Sigstore 72 | uses: sigstore/gh-action-sigstore-python@v3.0.0 73 | with: 74 | inputs: >- 75 | ./dist/*.tar.gz 76 | ./dist/*.whl 77 | - name: Create GitHub Release 78 | env: 79 | GITHUB_TOKEN: ${{ github.token }} 80 | run: >- 81 | gh release create 82 | '${{ github.ref_name }}' 83 | --repo '${{ github.repository }}' 84 | --notes "" 85 | - name: Upload artifact signatures to GitHub Release 86 | env: 87 | GITHUB_TOKEN: ${{ github.token }} 88 | run: >- 89 | gh release upload 90 | '${{ github.ref_name }}' dist/** 91 | --repo '${{ github.repository }}' 92 | -------------------------------------------------------------------------------- /.github/workflows/requirements.yml: -------------------------------------------------------------------------------- 1 | name: Dev requirements for target python 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test_python: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: recursive 16 | - name: Set up Python 3.13 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.13" 20 | - name: Install package (no deps) 21 | run: | 22 | curl -LsSf https://astral.sh/uv/install.sh | sh 23 | uv pip install --system --no-deps .[fitting,dev] 24 | uv pip install --system torch --index-url https://download.pytorch.org/whl/cpu 25 | - name: Install dev requirements 26 | run: | 27 | uv pip install --system -r requirements/dev-requirements.txt 28 | - name: Test with pytest 29 | run: | 30 | pytest --nbmake 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary and binary files 2 | *~ 3 | *.py[cod] 4 | *.so 5 | *.cfg 6 | !.isort.cfg 7 | !setup.cfg 8 | *.orig 9 | *.log 10 | *.pot 11 | __pycache__/* 12 | .cache/* 13 | .*.swp 14 | */.ipynb_checkpoints/* 15 | .ipynb_checkpoints 16 | .DS_Store 17 | .mypy_cache 18 | 19 | # Project files 20 | .ropeproject 21 | .project 22 | .pydevproject 23 | .settings 24 | .idea 25 | tags 26 | 27 | # Package files 28 | *.egg 29 | *.eggs/ 30 | .installed.cfg 31 | *.egg-info 32 | 33 | # Unittest and coverage 34 | htmlcov/* 35 | .coverage 36 | .coverage.* 37 | .tox 38 | junit*.xml 39 | coverage.xml 40 | .pytest_cache/ 41 | 42 | # Build and docs folder/files 43 | build/* 44 | dist/* 45 | sdist/* 46 | docs/api/* 47 | docs/_rst/* 48 | docs/_build/* 49 | docs/auto_examples/* 50 | cover/* 51 | MANIFEST 52 | 53 | # Per-project virtualenvs 54 | .venv*/ 55 | .conda*/ 56 | 57 | # Ignore pyenv virtualenv python version 58 | .python-version 59 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/elli/refractiveindexinfo-database"] 2 | path = src/elli/database/refractiveindexinfo-database 3 | url = https://github.com/polyanskiy/refractiveindex.info-database 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.8.5 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | # Run the formatter. 9 | - id: ruff-format 10 | - repo: local 11 | hooks: 12 | - id: sphinx-lint 13 | name: sphinx-lint 14 | entry: sphinx-lint 15 | language: system 16 | types: [python] 17 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-22.04 9 | tools: 10 | python: "3.11" 11 | 12 | # Build documentation in the docs/ directory with Sphinx 13 | sphinx: 14 | configuration: docs/conf.py 15 | 16 | submodules: 17 | include: all 18 | recursive: true 19 | 20 | # Optionally build your docs in additional formats such as PDF 21 | formats: 22 | - pdf 23 | 24 | python: 25 | install: 26 | - requirements: docs/requirements.txt 27 | -------------------------------------------------------------------------------- /.vscode/ltex.dictionary.en-US.txt: -------------------------------------------------------------------------------- 1 | dispersions 2 | dataframes 3 | ellipsometry 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [90], 3 | "editor.renderWhitespace": "all", 4 | "editor.tabSize": 4, 5 | "files.trimTrailingWhitespace": true, 6 | "[python]": { 7 | "editor.formatOnSave": true, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": "explicit", 10 | "source.organizeImports": "explicit" 11 | }, 12 | "editor.defaultFormatter": "charliermarsh.ruff" 13 | }, 14 | "python.testing.pytestArgs": ["tests"], 15 | "python.testing.unittestEnabled": false, 16 | "python.testing.pytestEnabled": true 17 | } -------------------------------------------------------------------------------- /.zenodo.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "pyElli", 3 | "description": "A matrix solver for 1D layered structures with an emphasis on day to day spectral ellipsometry tasks.", 4 | "license": "GPL-3.0-or-later", 5 | "upload_type": "software", 6 | "creators": [ 7 | { 8 | "name": "Müller, Marius J.", 9 | "orcid": "0009-0005-2187-0122" 10 | }, 11 | { 12 | "name": "Dobener, Florian", 13 | "orcid": "0000-0003-1987-6224" 14 | } 15 | ], 16 | "keywords": [ 17 | "python", 18 | "simulation", 19 | "solver", 20 | "ellipsometry", 21 | "transfer-matrix-method", 22 | "berreman4x4" 23 | ], 24 | "access_right": "open" 25 | } -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | - Marius Müller 4 | - Florian Dobener 5 | 6 | # Acknowledgements 7 | 8 | - Olivier Castany and C. Molinaro [Berreman4x4](https://github.com/Berreman4x4/Berreman4x4) 9 | - Solver2x2 based on Steve Byrnes' [tmm](https://github.com/sbyrnes321/tmm) 10 | - Mikhail Polyanskiy's [refractiveindex.info database](https://github.com/polyanskiy/refractiveindex.info-database) and Pavel Dmitriev's [pyTMM](https://github.com/kitchenknif/PyTMM) for his importer script for the database 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jupyter/scipy-notebook 2 | 3 | ENV HOME=/home/jovyan 4 | WORKDIR $HOME 5 | 6 | RUN pip install pyElli[fitting] 7 | 8 | WORKDIR $HOME/work 9 | ADD examples ./ -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/elli/database/refractiveindexinfo-database *.yml 2 | include src/elli/formula_parser/dispersion_function_grammer.lark -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | The pyElli logo 3 |
4 | 5 | -------- 6 | 7 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyElli) [![PyPI](https://img.shields.io/pypi/v/pyElli)](https://pypi.org/project/pyElli/) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5702469.svg)](https://doi.org/10.5281/zenodo.5702469) [![status](https://joss.theoj.org/papers/515ecaee405aa0be1cb9b887fc5e21bb/status.svg)](https://joss.theoj.org/papers/515ecaee405aa0be1cb9b887fc5e21bb) [![Pytest](https://github.com/PyEllips/pyElli/actions/workflows/pytest.yml/badge.svg)](https://github.com/PyEllips/pyElli/actions/workflows/pytest.yml) [![Documentation Status](https://readthedocs.org/projects/pyelli/badge/?version=latest)](https://pyelli.readthedocs.io/en/latest/?badge=latest) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![](https://dcbadge.vercel.app/api/server/zCBNMtBFAQ?style=flat&compact=true)](https://discord.gg/zCBNMtBFAQ) 8 | 9 | # pyElli 10 | 11 | PyElli is an open source numerical solver for spectral ellipsometry employing well-known 2x2 and 4x4 algorithms. 12 | It is intended for a broad case of problems including simple fitting of layered structures, anisotropic layers and any other light interaction with layered 1D structures. 13 | It serves as a system for the day to day ellipsometry task at hand and is easily extendable with your own dispersion models, EMAs or solvers. 14 | Our goal is to provide a reproducible and flexible tool for the needs 15 | of scientists working with spectral ellipsometry. 16 | 17 | ## Features 18 | 19 | - A multitude of models to approximate the dielectric function of your material. 20 | - Use the vast library of materials from refractiveindex.info as reference materials. 21 | - Build up your structure easily from materials and layers. 22 | - Simulate reflection and transmission spectra, ellipsometric parameters and Mueller matrices. 23 | - Utilities to quickly convert, plot and fit your measurement data. 24 | - Powerful when necessary, editable and expandable. 25 | 26 | ## Got a question? 27 | 28 | If you have questions using pyElli please feel free to open a discussion in the [Q&A](https://github.com/PyEllips/pyElli/discussions/categories/q-a) or join our [discord channel](https://discord.gg/zCBNMtBFAQ). 29 | 30 | ## How to get it 31 | 32 | The installers for all releases are available at the [Python Package Index (PyPI)](https://pypi.org/project/pyElli/). 33 | 34 | To install run: 35 | 36 | ```sh 37 | pip install pyElli[fitting] 38 | ``` 39 | 40 | This installs pyElli with the additional fitting capabilities and interactive widgets. 41 | If you don't want to have this functionality just drop the `[fitting]` in the end. 42 | 43 | To increase performance of the 4x4 Solver, it is recommended to 44 | install PyTorch manually, as it is too big to include in the standard installation. 45 | Installation information can be found at the [PyTorch Website](https://pytorch.org/get-started/locally/). 46 | The CPU variant is sufficient, if you want to save some space. 47 | 48 | A complete environment for pyElli is also available as a [Docker Container](https://hub.docker.com/r/domna/pyelli). 49 | To pull and run it directly just execute 50 | 51 | ```sh 52 | docker run -p 8888:8888 domna/pyelli 53 | ``` 54 | 55 | from your local docker install. After startup a link should 56 | appear in your console. Click it and you will be directed 57 | to a jupyter server with the latest release of pyElli available. 58 | 59 | To install the latest development version use: 60 | 61 | ```sh 62 | pip install "pyElli[fitting] @ git+https://github.com/PyEllips/pyElli.git" 63 | ``` 64 | 65 | The source code is hosted on [GitHub](https://github.com/PyEllips/pyElli), to manually install from source, clone the repository and run `pip install -e .` in 66 | the folder to install it in development mode: 67 | 68 | ```sh 69 | git clone https://github.com/PyEllips/pyElli 70 | cd pyElli 71 | pip install -e ".[fitting]" 72 | ``` 73 | 74 | ## How to cite 75 | 76 | Until we have published a Paper on pyElli, we have prepared a Zenodo entry with DOIs for every pyElli Version. The can be found [here](https://zenodo.org/records/13903325). 77 | 78 | 79 | ## Acknowledgements 80 | 81 | - Based on Olivier Castany's [Berreman4x4](https://github.com/Berreman4x4/Berreman4x4) 82 | - Solver2x2 based on Steve Byrnes' [tmm](https://github.com/sbyrnes321/tmm) 83 | - Mikhail Polyanskiy's [refractiveindex.info database](https://github.com/polyanskiy/refractiveindex.info-database) and Pavel Dmitriev's [pyTMM](https://github.com/kitchenknif/PyTMM) for his importer script for the database 84 | 85 | [@MarJMue](https://github.com/MarJMue) received financial support from 2021 until 2025 by the Deutsche Forschungsgemeinschaft (DFG, German Research Foundation), grant No. 398143140 (FOR 2824). 86 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | AUTODOCDIR = api 11 | 12 | # User-friendly check for sphinx-build 13 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1) 14 | $(error "The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/") 15 | endif 16 | 17 | .PHONY: help clean Makefile 18 | 19 | # Put it first so that "make" without argument is like "make help". 20 | help: 21 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | 23 | clean: 24 | rm -rf $(BUILDDIR)/* $(AUTODOCDIR) 25 | rm -rf auto_examples 26 | 27 | # Catch-all target: route all unknown targets to Sphinx using the new 28 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 29 | %: Makefile 30 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 31 | -------------------------------------------------------------------------------- /docs/_static/basic_usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/docs/_static/basic_usage.png -------------------------------------------------------------------------------- /docs/_static/custom_fitting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/docs/_static/custom_fitting.png -------------------------------------------------------------------------------- /docs/_static/mueller_matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/docs/_static/mueller_matrix.png -------------------------------------------------------------------------------- /docs/_static/multilayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/docs/_static/multilayer.png -------------------------------------------------------------------------------- /docs/_static/style.css: -------------------------------------------------------------------------------- 1 | div.sphx-glr-download-link-note { 2 | height: 0px; 3 | visibility: hidden; 4 | } 5 | 6 | p.sphx-glr-timing { 7 | height: 0px; 8 | visibility: hidden; 9 | } 10 | 11 | div.sphx-glr-download { 12 | height: 0px; 13 | visibility: hidden; 14 | } 15 | 16 | p.sphx-glr-signature { 17 | height: 0px; 18 | visibility: hidden; 19 | } 20 | 21 | dl.field-list .colon { 22 | display: none; 23 | } 24 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. _authors: 2 | .. mdinclude:: ../AUTHORS.md 3 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changes: 2 | .. mdinclude:: ../CHANGELOG.md 3 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../CONTRIBUTING.md 2 | -------------------------------------------------------------------------------- /docs/database.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Dispersion Database 3 | =================== 4 | 5 | .. autoclass:: elli.db.RII 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/experiment.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Experiment 3 | ========== 4 | 5 | .. automodule:: elli.experiment 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | -------------------------------------------------------------------------------- /docs/helpers.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Helper functions 3 | ================= 4 | Helper functions for generation rotation matrices and 5 | conversions for wavelengths and ellipsometric quantities. 6 | 7 | .. automodule:: elli.utils 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | pyElli 3 | ====== 4 | 5 | PyElli is an open-source numerical solver for spectral ellipsometry employing well-known 6 | 2x2 and 4x4 solver algorithms. It is intended for a broad range of problems 7 | such as simple fitting of layered structures, anisotropic layers and any 8 | other polarized light interaction with layered 1D structures. 9 | It serves as a system for the day to day ellipsometry task at hand 10 | and aims to make optical model generation standardized and reproducible. 11 | 12 | PyElli is build to be easily extendable by optical models. 13 | However, pyElli comes with batteries included and already offers a wide range 14 | of :doc:`dispersion models` and the material database of Refractiveindex.info. 15 | 16 | Most of the models presented in the 17 | comprehensive book of Fujiwara and Collins [1]_ are present and additionally 18 | a lot of other models used by ellipsometry vendor software are included. 19 | 20 | The material database offers the dispersions seen on the `website `_ 21 | and can be accessed by using the :class:`elli.db.RII` module. 22 | 23 | To start you may want to dive into :ref:`install`. 24 | The bast way to start is to have a look at the :doc:`basic usage` or 25 | the :doc:`other examples`. 26 | 27 | PyElli consists of a set of classes which work together to create a full 28 | light interaction experiment. 29 | In the image below you see the set of different classes and how they work together 30 | to evaluate a modeled system. 31 | 32 | .. mermaid:: 33 | 34 | graph LR 35 | Dx(Dispersion x) --> AM 36 | Dy(Dispersion y) --> AM 37 | Dz(Dispersion z) --> AM 38 | D(Dispersion) --> M 39 | M(Material) --> L 40 | AM(AnisotropicMaterial) --> L 41 | M -- front Material --> S 42 | AM -- back Material --> S 43 | L(Layers) --> S & S 44 | S(Structure) --> E(Experiment) 45 | S --> |evaluate| R(Result) 46 | E --> R 47 | 48 | It starts by building a set of dispersions and plugging them into materials classes the specific 49 | number of dispersions depends on whether it is an :class:`IsotropicMaterial` or an :class:`AnisotropicMaterial`. 50 | These materials classes also support creating effective medium layers for inclusions or roughnesses. 51 | The next step is building a :class:`Structure` from these materials. 52 | The :class:`Structure` needs as least two materials for the incoming and outgoing materials, 53 | but can contain arbitrary more :class:`layers` which are only limited by the computational resources. 54 | The :class:`VaryingMixtureLayer` class can also account for gradient changes of materials in z-direction 55 | of a layer, which is useful for gradient layers or roughness modeling. 56 | As the last step the :class:`Structure` is plugged into an :class:`Experiment`, which contains 57 | the experimental conditions, such as light polarization. 58 | By evaluating the experiment a :class:`Result` class containing the calculated data is returned. 59 | The creation of an experiment can be skipped by calling the :meth:`evaluate` method directly 60 | on a :class:`Structure` class if you want to use standard experimental settings. 61 | 62 | .. rubric:: References 63 | 64 | .. [1] H. Fujiwara and R. W. Collins, 65 | Spectroscopic Ellipsometry for Photovoltaics, 66 | Volume 1: Fundamental Principles and Solar Cell Characterization, 67 | Ed. 1, Springer Series in Optical Sciences 212 (2018). 68 | https://doi.org/10.1007/978-3-319-75377-5 69 | 70 | 71 | 72 | Contents 73 | ======== 74 | 75 | .. toctree:: 76 | :maxdepth: 2 77 | 78 | installation 79 | auto_examples/index 80 | modules 81 | 82 | Misc 83 | ==== 84 | 85 | .. toctree:: 86 | :maxdepth: 1 87 | 88 | Contributions & Help 89 | License 90 | Authors 91 | Changelog 92 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ======== 4 | Install 5 | ======== 6 | The installers for all releases are available at the 7 | `Python package Index (PyPI) `_. 8 | 9 | To install the package in your current virtual environment execute 10 | 11 | .. code-block:: shell 12 | 13 | pip install pyElli[fitting] 14 | 15 | This installs pyElli with the additional fitting capabilities and interactive widgets. 16 | If you don't want to have this functionality just drop the `[fitting]` in the end. 17 | 18 | To increase performance of the 4x4 Solver, it is recommended to 19 | install PyTorch manually, as it is too big to include in the standard installation. 20 | Installation information can be found at the `PyTorch Website `_. 21 | The CPU variant is sufficient, if you want to save some space. 22 | 23 | A complete environment for pyElli is also available as a 24 | `Docker Container `_. 25 | To pull and run it directly just execute 26 | 27 | .. code-block:: shell 28 | 29 | docker run -p 8888:8888 domna/pyelli 30 | 31 | from your local docker install. After startup a link should 32 | appear in your console. Click it and you will be directed 33 | to a jupyter server with the latest release of pyElli available. 34 | 35 | To install the latest development version use: 36 | 37 | .. code-block:: shell 38 | 39 | pip install git+https://github.com/pyEllips/pyElli.git 40 | 41 | The source code is hosted on `GitHub `_, 42 | to manually install from source, clone the repository and run `pip install -e .` in 43 | the folder to install it in development mode: 44 | 45 | .. code-block:: shell 46 | 47 | git clone https://github.com/PyEllips/pyElli 48 | cd pyElli 49 | pip install -e . 50 | -------------------------------------------------------------------------------- /docs/kkr.rst: -------------------------------------------------------------------------------- 1 | Kramers-Kronig relations 2 | ============================== 3 | 4 | .. automodule:: elli.kkr.kkr 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | ======= 4 | License 5 | ======= 6 | 7 | .. literalinclude:: ../LICENSE.txt 8 | -------------------------------------------------------------------------------- /docs/materials.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Materials & EMA 3 | ================ 4 | 5 | .. automodule:: elli.materials 6 | 7 | Abstract base classes 8 | --------------------- 9 | .. autoclass:: elli.materials.Material 10 | :members: 11 | 12 | .. autoclass:: elli.materials.MixtureMaterial 13 | :members: 14 | 15 | .. autoclass:: elli.materials.SingleMaterial 16 | :members: 17 | 18 | Isotropic and non-isotropic materials 19 | ------------------------------------- 20 | .. autoclass:: elli.materials.IsotropicMaterial 21 | :members: 22 | 23 | .. autoclass:: elli.materials.UniaxialMaterial 24 | :members: 25 | 26 | .. autoclass:: elli.materials.BiaxialMaterial 27 | :members: 28 | 29 | Effective medium approximations 30 | ------------------------------- 31 | .. autoclass:: elli.materials.VCAMaterial 32 | :members: 33 | 34 | .. autoclass:: elli.materials.BruggemanEMA 35 | :members: 36 | 37 | .. autoclass:: elli.materials.MaxwellGarnettEMA 38 | :members: 39 | 40 | .. autoclass:: elli.materials.LooyengaEMA 41 | :members: 42 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | .. _modules: 2 | 3 | ************* 4 | API Reference 5 | ************* 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | database 11 | dispersions 12 | materials 13 | structure 14 | experiment 15 | solvers 16 | result 17 | plot 18 | read_write 19 | helpers 20 | kkr 21 | -------------------------------------------------------------------------------- /docs/plot.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Fitting and plotting 3 | ==================== 4 | 5 | Interactive fitting 6 | ------------------- 7 | PyElli offers several classes and decorators to make fitting easy. 8 | The central idea is to construct a class containing the measurement data and an optical model 9 | which is fitted to the data with `lmfit `_. 10 | Since pyElli uses lmfit under the hood you may take advantage of its vast capabilities. 11 | 12 | To make creation of the fitting classes as easy as possible pyElli contains decorators to 13 | automatically instantiate the class by providing a function containing the optical model. 14 | 15 | Here you see an example of invoking such a decorator with a measurement dataframe **psi_delta** and 16 | parameters **params**, an lmfit `Parameter `_ 17 | or :class:`ParamsHist` object to create a 18 | :class:`FitRho` class. 19 | 20 | .. code-block:: python 21 | 22 | @fit(psi_delta, params) 23 | def model(lbda, params): 24 | ... 25 | 26 | In the :code:`model` function the actual optical model should be constructed and an 27 | :class:`Experiment` object 28 | should be returned. 29 | A detailed example on how to use this decorator you find in the :doc:`basic usage` example. 30 | 31 | Psi/Delta fitting 32 | ^^^^^^^^^^^^^^^^^ 33 | Fitting decorator and class to fit Psi/Delta experiments. 34 | 35 | .. automodule:: elli.fitting.decorator_psi_delta 36 | :members: 37 | :show-inheritance: 38 | 39 | Mueller matrix fitting 40 | ^^^^^^^^^^^^^^^^^^^^^^ 41 | Fitting decorator and class to fit mueller matrix experiments. 42 | 43 | .. automodule:: elli.fitting.decorator_mmatrix 44 | :members: 45 | :show-inheritance: 46 | 47 | Fitting base class 48 | ^^^^^^^^^^^^^^^^^^ 49 | This is the base class providing basic fitting features. 50 | This class is not intended to be used directly, it rather should 51 | be inherited from in additional fitting classes. 52 | 53 | .. automodule:: elli.fitting.decorator 54 | :members: 55 | :show-inheritance: 56 | 57 | Parameter class 58 | --------------- 59 | The parameter class extending lmfit's Parameter class by a history 60 | of parameter changes. 61 | 62 | .. automodule:: elli.fitting.params_hist 63 | :members: 64 | :show-inheritance: 65 | 66 | Plotting 67 | -------- 68 | 69 | Mueller matrix 70 | ^^^^^^^^^^^^^^ 71 | This is a helper class to plot a mueller matrix dataframe. 72 | 73 | .. automodule:: elli.plot.mueller_matrix 74 | :members: 75 | :show-inheritance: 76 | 77 | Structure 78 | ^^^^^^^^^ 79 | Plots a refractive index slice through a stack of materials. 80 | 81 | .. automodule:: elli.plot.structure 82 | :members: 83 | :show-inheritance: 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/read_write.rst: -------------------------------------------------------------------------------- 1 | Reading and writing 2 | =================== 3 | 4 | .. automodule:: elli.importer.nexus 5 | :members: 6 | 7 | .. automodule:: elli.importer.spectraray 8 | :members: 9 | 10 | .. automodule:: elli.importer.woollam 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/result.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | Result 3 | ====== 4 | 5 | .. automodule:: elli.result 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | -------------------------------------------------------------------------------- /docs/solvers.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Solvers 3 | ******* 4 | 5 | For calculation of light interaction in material stacks the transfer matrix method is used. 6 | In pyElli the :class:`Solver` classes provide the necessary toolset for two kinds of transfer matrix algorithms. 7 | They are not intended to be used directly, but rather to be provided in the evaluation in the :class:`Structure` class. 8 | The :class:`Solver2x2` is a simple and fast algorithm for isotropic materials. 9 | It splits the calculation into two 2x2 matrices, one for the s and one for the p polarized light. 10 | 11 | The :class:`Solver4x4` is a more complex algorithm for anisotropic materials. 12 | It employs a full 4x4 matrix formulation for all light interaction. 13 | It is based on the Berreman matrix formalism [1]_. 14 | In the Berreman formalism a propagator for matrix exponentials is needed. 15 | pyElli provides different implementations to be used in the calculation of the transfer matrices. 16 | The :class:`PropagatorEig` is based on solving the eigenvalues of the first order approximation of the matrix exponential. 17 | Although, it is very fast it is not very accurate. 18 | The :class:`PropagatorExpm` is solving the matrix exponential by the Pade approximation. 19 | It can use SciPy as backend, but for performance-critical tasks, it is recommended to install PyTorch. 20 | 21 | .. rubric:: References 22 | 23 | .. [1] Dwight W. Berreman, "Optics in Stratified and Anisotropic Media: 4×4-Matrix Formulation," J. Opt. Soc. Am. 62, 502-510 (1972) 24 | 25 | 26 | 27 | Solver base class (Solver) 28 | ========================== 29 | 30 | .. automodule:: elli.solver 31 | :members: 32 | :undoc-members: 33 | :show-inheritance: 34 | 35 | 2x2 Matrix Solver (Solver2x2) 36 | ============================= 37 | 38 | .. automodule:: elli.solver2x2 39 | :members: 40 | :undoc-members: 41 | :show-inheritance: 42 | 43 | 4x4 Matrix Solver (Solver4x4) 44 | ============================= 45 | 46 | .. automodule:: elli.solver4x4 47 | :members: 48 | :undoc-members: 49 | :show-inheritance: 50 | -------------------------------------------------------------------------------- /docs/structure.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Structures 3 | ========== 4 | 5 | .. automodule:: elli.structure 6 | 7 | Layers 8 | ------ 9 | 10 | Abstract base classes 11 | ~~~~~~~~~~~~~~~~~~~~~ 12 | 13 | .. autoclass:: elli.structure.AbstractLayer 14 | :members: 15 | 16 | .. autoclass:: elli.structure.InhomogeneousLayer 17 | :members: 18 | 19 | Homogeneous layers 20 | ~~~~~~~~~~~~~~~~~~ 21 | 22 | .. autoclass:: elli.structure.Layer 23 | :members: 24 | 25 | .. autoclass:: elli.structure.RepeatedLayers 26 | :members: 27 | 28 | Inhomogeneous layers 29 | ~~~~~~~~~~~~~~~~~~~~ 30 | 31 | .. autoclass:: elli.structure.TwistedLayer 32 | :members: 33 | 34 | .. autoclass:: elli.structure.VaryingMixtureLayer 35 | :members: 36 | 37 | Structure Class 38 | --------------- 39 | 40 | .. autoclass:: elli.structure.Structure 41 | :members: 42 | -------------------------------------------------------------------------------- /examples/Basic Usage/SiO2onSi.ellips.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/examples/Basic Usage/SiO2onSi.ellips.nxs -------------------------------------------------------------------------------- /examples/Custom fitting/SiO2onSi.ellips.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/examples/Custom fitting/SiO2onSi.ellips.nxs -------------------------------------------------------------------------------- /examples/Ellipsometry uniaxial materials/Fujiwara-641.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # encoding: utf-8 3 | # %% [markdown] 4 | # # We reproduce results from section 6.4.1 in 'Spectroscopic Ellipsometry', by H. Fujiwara. 5 | # 6 | # Author: O. Castany, M. Müller 7 | # 8 | # Verification of the code against results presented in Fujiwara's book 'Spectroscopic Ellipsometry', section 6.4.1 9 | # (p. 237). We reproduce figures 6.16 and 6.17. 10 | 11 | # %% 12 | import numpy as np 13 | import elli 14 | import matplotlib.pyplot as plt 15 | 16 | # %% [markdown] 17 | # ## Setting up the materials 18 | 19 | # %% 20 | # Front half-space (air) 21 | air = elli.AIR 22 | 23 | # Anisotropic substrate 24 | n_o = 2.0 # ordinary index of thin layer 25 | n_e = 2.5 # extraordinary index of thin layer 26 | 27 | uniaxialMaterial = elli.UniaxialMaterial( 28 | elli.ConstantRefractiveIndex(n_o), elli.ConstantRefractiveIndex(n_e) 29 | ) 30 | 31 | # %% [markdown] 32 | # ## We reproduce figure 6.16 (p. 238) 33 | # %% 34 | # Orientations of the anisotropic substrate 35 | Φ_E = 90 #  1st Euler angle 36 | θ_E_list = [0, 45, 90] #  2nd Eulet angle 37 | 38 | # Incidence angles 39 | Φ_i_list = np.linspace(0, 89, 300) #  array of Φ_i values 40 | 41 | data = elli.ResultList() 42 | 43 | for θ_E in θ_E_list: 44 | R = elli.rotation_euler(Φ_E, θ_E, 0) 45 | uniaxialMaterial.set_rotation(R) 46 | s = elli.Structure(air, [], uniaxialMaterial) 47 | for Φ_i in Φ_i_list: 48 | data.append(s.evaluate(500, Φ_i)) 49 | 50 | Psi = data.psi_pp.reshape(3, 300).T 51 | 52 | fig = plt.figure() 53 | ax = fig.add_subplot(111) 54 | ax.plot(Φ_i_list, Psi) 55 | ax.set_xlabel(r"$\theta_i$") 56 | ax.set_ylabel(r"$\Psi_{pp}$") 57 | 58 | plt.tight_layout() 59 | 60 | # %% [markdown] 61 | # ## We reproduce figure 6.17 (p. 239) 62 | 63 | # %% 64 | Φ_i = 70 #  70° incidence angle 65 | 66 | # Orientations of the anisotropic substrate 67 | Φ_E_list = np.linspace(0, 360, 36 * 2 + 1) #  1st Euler angle 68 | θ_E_list = [0, 45, 90] #  2nd Euler angle 69 | 70 | data2 = elli.ResultList() 71 | 72 | for θ_E in θ_E_list: 73 | for Φ_E in Φ_E_list: 74 | R = elli.rotation_euler(Φ_E, θ_E, 0) 75 | uniaxialMaterial.set_rotation(R) 76 | s = elli.Structure(air, [], uniaxialMaterial) 77 | data2.append(s.evaluate(500, Φ_i)) 78 | 79 | Psi = data2.psi_pp.reshape(3, 73).T 80 | 81 | fig = plt.figure() 82 | ax = fig.add_subplot(111) 83 | ax.plot(Φ_E_list, Psi) 84 | ax.set_xlim(0, 360) 85 | ax.set_xlabel(r"$\phi_E$") 86 | ax.set_ylabel(r"$\Psi_{pp}$") 87 | 88 | plt.tight_layout() 89 | plt.show() 90 | 91 | # %% 92 | -------------------------------------------------------------------------------- /examples/Interfaces/interface-reflection.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "%matplotlib inline" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "\n# Interface reflection\n" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "## Interface between two materials\nAuthors: O. Castany, C. Molinaro, M. M\u00fcller\n\nInterface between two materials n1/n2.\nCalculations of the transmission and reflexion coefficients with varying incidence angle.\n\n" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": { 32 | "collapsed": false 33 | }, 34 | "outputs": [], 35 | "source": [ 36 | "import elli\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom scipy.constants import c, pi" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "## Structure definition\n\n" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": { 50 | "collapsed": false 51 | }, 52 | "outputs": [], 53 | "source": [ 54 | "n1 = 1\nn2 = 1.5\nfront = elli.IsotropicMaterial(elli.ConstantRefractiveIndex(n1))\nback = elli.IsotropicMaterial(elli.ConstantRefractiveIndex(n2))\n\n# Structure\ns = elli.Structure(front, [], back)\n\n# Parameters for the calculation\nlbda = 1000\nk0 = 2 * pi / lbda\nPhi_list = np.linspace(0, 89, 90) # \u00a0range for the incidence angles" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "## Analytical calculation\n\n" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": { 68 | "collapsed": false 69 | }, 70 | "outputs": [], 71 | "source": [ 72 | "Phi_i = np.deg2rad(Phi_list)\n\nPhi_t = np.arcsin((n1 * np.sin(Phi_i) / n2).astype(complex))\nkz1 = n1 * k0 * np.cos(Phi_i)\nkz2 = n2 * k0 * np.cos(Phi_t)\nr_s = (kz1 - kz2) / (kz1 + kz2)\nt_s = 1 + r_s\nr_p = (kz1 * n2**2 - kz2 * n1**2) / (kz1 * n2**2 + kz2 * n1**2)\nt_p = np.cos(Phi_i) * (1 - r_p) / np.cos(Phi_t)\n\n# Reflection and transmission coefficients, polarisation s and p\nR_th_ss = abs(r_s) ** 2\nR_th_pp = abs(r_p) ** 2\nt2_th_ss = abs(t_s) ** 2\nt2_th_pp = abs(t_p) ** 2\n# The power transmission coefficient is T = Re(kz2/kz1) \u00d7 |t|^2\ncorrection = np.real(kz2 / kz1)\nT_th_ss = correction * t2_th_ss\nT_th_pp = correction * t2_th_pp" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "## Calculation with pyElli\n\n" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": { 86 | "collapsed": false 87 | }, 88 | "outputs": [], 89 | "source": [ 90 | "data = elli.ResultList([s.evaluate(lbda, Phi_i) for Phi_i in Phi_list])\n\nR_pp = data.R_pp\nR_ss = data.R_ss\n\nT_pp = data.T_pp\nT_ss = data.T_ss\n\nt2_pp = np.abs(data.t_pp) ** 2\nt2_ss = np.abs(data.t_ss) ** 2" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "## Plotting\n\n" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": { 104 | "collapsed": false 105 | }, 106 | "outputs": [], 107 | "source": [ 108 | "fig = plt.figure(figsize=(12.0, 6.0))\nplt.rcParams[\"axes.prop_cycle\"] = plt.cycler(\"color\", \"bgrcmk\")\nax = fig.add_axes([0.1, 0.1, 0.7, 0.8])\n\nd = np.vstack((R_ss, R_pp, t2_ss, t2_pp, T_ss, T_pp)).T\nlines1 = ax.plot(Phi_list, d)\nlegend1 = (\"R_ss\", \"R_pp\", \"t2_ss\", \"t2_pp\", \"T_ss\", \"T_pp\")\n\nd = np.vstack((R_th_ss, R_th_pp, t2_th_ss, t2_th_pp, T_th_ss, T_th_pp)).T\nlines2 = ax.plot(Phi_list, d, \".\")\nlegend2 = (\"R_th_ss\", \"R_th_pp\", \"t2_th_ss\", \"t2_th_pp\", \"T_th_ss\", \"T_th_pp\")\n\nax.legend(\n lines1 + lines2,\n legend1 + legend2,\n loc=\"upper left\",\n bbox_to_anchor=(1.05, 1),\n borderaxespad=0.0,\n)\n\nax.set_title(\"Interface n$_1$={:} / n$_2$={:}\".format(n1, n2))\nax.set_xlabel(r\"Incidence angle $\\Phi_i$ \")\nax.set_ylabel(r\"Reflexion and transmission coefficients $R$, $T$, $|t|^2$\")\n\nplt.show()" 109 | ] 110 | } 111 | ], 112 | "metadata": { 113 | "kernelspec": { 114 | "display_name": "Python 3", 115 | "language": "python", 116 | "name": "python3" 117 | }, 118 | "language_info": { 119 | "codemirror_mode": { 120 | "name": "ipython", 121 | "version": 3 122 | }, 123 | "file_extension": ".py", 124 | "mimetype": "text/x-python", 125 | "name": "python", 126 | "nbconvert_exporter": "python", 127 | "pygments_lexer": "ipython3", 128 | "version": "3.9.13" 129 | } 130 | }, 131 | "nbformat": 4, 132 | "nbformat_minor": 0 133 | } -------------------------------------------------------------------------------- /examples/Liquid crystals/cholesteric-liquid.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "%matplotlib inline" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "\n# Cholesteric Liquid Crystal\n" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "## Example of a cholesteric liquid crystal\nAuthors: O. Castany, C. Molinaro, M. M\u00fcller\n\n" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": { 32 | "collapsed": false 33 | }, 34 | "outputs": [], 35 | "source": [ 36 | "import elli\nimport elli.plot as elliplot\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom scipy.constants import c, pi" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "## Setup materials and structure\n\n" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": { 50 | "collapsed": false 51 | }, 52 | "outputs": [], 53 | "source": [ 54 | "glass = elli.IsotropicMaterial(elli.ConstantRefractiveIndex(1.55))\nfront = back = glass\n\n# Liquid crystal oriented along the x direction\n(no, ne) = (1.5, 1.7)\nDn = ne - no\nn_med = (ne + no) / 2\nLC = elli.UniaxialMaterial(\n elli.ConstantRefractiveIndex(no), elli.ConstantRefractiveIndex(ne)\n) # ne is along z\nR = elli.rotation_v_theta(elli.E_Y, 90) # rotation of pi/2 along y\nLC.set_rotation(R) # apply rotation from z to x\n\n# Cholesteric pitch (nm):\np = 650\n\n# One half turn of a right-handed helix:\nTN = elli.TwistedLayer(LC, p / 2, angle=180, div=35)\n\n# Repetition the helix layer\nN = 15 # number half pitch repetitions\nh = N * p / 2\nL = elli.RepeatedLayers([TN], N)\ns = elli.Structure(front, [L], back)\n\n# Calculation parameters\nlbda_min, lbda_max = 800, 1200 # (nm)\nlbda_B = p * n_med\nlbda_list = np.linspace(lbda_min, lbda_max, 100)" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "## Analytical calculation for the maximal reflection\n\n" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": { 68 | "collapsed": false 69 | }, 70 | "outputs": [], 71 | "source": [ 72 | "R_th = np.tanh(Dn / n_med * pi * h / p) ** 2\nlbda_B1, lbda_B2 = p * no, p * ne" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "## Calculation with pyElli\n\n" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": { 86 | "collapsed": false 87 | }, 88 | "outputs": [], 89 | "source": [ 90 | "data = s.evaluate(lbda_list, 0)\n\nT_pp = data.T_pp\nT_ps = data.T_ps\nT_ss = data.T_ss\nT_sp = data.T_sp\n\n# Transmission coefficients for incident unpolarized light:\nT_pn = 0.5 * (T_pp + T_ps)\nT_sn = 0.5 * (T_sp + T_ss)\nT_nn = T_sn + T_pn\n\n# Transmission coefficients for 's' and 'p' polarized light, with\n# unpolarized measurement.\nT_ns = T_ps + T_ss\nT_np = T_pp + T_sp\n\n# Right-circular wave is reflected in the stop-band.\n# R_LR, T_LR close to zero.\nR_RR = data.Rc_RR\nR_LR = data.Rc_LR\nT_RR = data.Tc_RR\nT_LR = data.Tc_LR\n\n# Left-circular wave is transmitted in the full spectrum.\n# T_RL, R_RL, R_LL close to zero, T_LL close to 1.\nT_LL = data.Tc_LL\nR_LL = data.Rc_LL" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "## Plotting\n\n" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": { 104 | "collapsed": false 105 | }, 106 | "outputs": [], 107 | "source": [ 108 | "fig = plt.figure()\nax = fig.add_subplot(1, 1, 1)\n\n# Draw rectangle for \u03bb \u2208 [p\u00b7no, p\u00b7ne], and T \u2208 [0, R_th]\nrectangle = plt.Rectangle((lbda_B1, 0), lbda_B2 - lbda_B1, R_th, color=\"cyan\")\nax.add_patch(rectangle)\n\nax.plot(lbda_list, R_RR, \"--\", label=\"R_RR\")\nax.plot(lbda_list, T_RR, label=\"T_RR\")\nax.plot(lbda_list, T_nn, label=\"T_nn\")\nax.plot(lbda_list, T_ns, label=\"T_ns\")\nax.plot(lbda_list, T_np, label=\"T_np\")\n\nax.legend(loc=\"center right\", bbox_to_anchor=(1.00, 0.50))\n\nax.set_title(\n \"Right-handed Cholesteric Liquid Crystal, aligned along \\n\"\n + \"the $x$ direction, with {:.1f} helix pitches.\".format(N / 2.0)\n)\nax.set_xlabel(r\"Wavelength $\\lambda_0$ (nm)\")\nax.set_ylabel(r\"Power transmission $T$ and reflexion $R$\")\nplt.show()\n\nelliplot.draw_structure(s)" 109 | ] 110 | } 111 | ], 112 | "metadata": { 113 | "kernelspec": { 114 | "display_name": "Python 3", 115 | "language": "python", 116 | "name": "python3" 117 | }, 118 | "language_info": { 119 | "codemirror_mode": { 120 | "name": "ipython", 121 | "version": 3 122 | }, 123 | "file_extension": ".py", 124 | "mimetype": "text/x-python", 125 | "name": "python", 126 | "nbconvert_exporter": "python", 127 | "pygments_lexer": "ipython3", 128 | "version": "3.9.13" 129 | } 130 | }, 131 | "nbformat": 4, 132 | "nbformat_minor": 0 133 | } -------------------------------------------------------------------------------- /examples/gallery/README.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | -------------------------------------------------------------------------------- /examples/gallery/SiO2onSi.ellips.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/examples/gallery/SiO2onSi.ellips.nxs -------------------------------------------------------------------------------- /examples/gallery/plot_02_TiO2_multilayer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multilayer fit 3 | ============== 4 | 5 | Fits a multilayer model to an ALD grown TiO2 sample on SiO2 / Si. 6 | """ 7 | 8 | # %% 9 | import elli 10 | from elli.fitting import ParamsHist, fit 11 | 12 | # sphinx_gallery_thumbnail_path = '_static/multilayer.png' 13 | 14 | # %% 15 | # Load data 16 | # --------- 17 | # 18 | # Load data collected with Sentech Ellipsometer and cut the spectral range (to use Si Aspnes file) 19 | # 20 | # The sample is an ALD grown TiO2 sample (with 400 cycles) 21 | # on commercially available SiO2 / Si substrate. 22 | tss = elli.read_spectraray_psi_delta("TiO2_400cycles.txt").loc[70.06][400:800] 23 | 24 | # %% 25 | # Set start parameters 26 | # -------------------- 27 | # Here we set the start parameters for the TiO2 and SiO2 layer. 28 | # We set the SiO2 layer parameters to a fixed value from another 29 | # fit of the substrate. See the :ref:`Basic usage` example for details 30 | # on how to perform such a fit. 31 | # In general it is a good idea to fit your data layer-wise if possible 32 | # to yield a better fit quality. 33 | params = ParamsHist() 34 | params.add("SiO2_n0", value=1.452, min=-100, max=100, vary=False) 35 | params.add("SiO2_n1", value=36.0, min=-40000, max=40000, vary=False) 36 | params.add("SiO2_n2", value=0, min=-40000, max=40000, vary=False) 37 | params.add("SiO2_k0", value=0, min=-100, max=100, vary=False) 38 | params.add("SiO2_k1", value=0, min=-40000, max=40000, vary=False) 39 | params.add("SiO2_k2", value=0, min=-40000, max=40000, vary=False) 40 | params.add("SiO2_d", value=276.36, min=0, max=40000, vary=False) 41 | 42 | params.add("TiO2_n0", value=2.236, min=-100, max=100, vary=True) 43 | params.add("TiO2_n1", value=451, min=-40000, max=40000, vary=True) 44 | params.add("TiO2_n2", value=251, min=-40000, max=40000, vary=True) 45 | params.add("TiO2_k0", value=0, min=-100, max=100, vary=False) 46 | params.add("TiO2_k1", value=0, min=-40000, max=40000, vary=False) 47 | params.add("TiO2_k2", value=0, min=-40000, max=40000, vary=False) 48 | 49 | params.add("TiO2_d", value=20, min=0, max=40000, vary=True) 50 | 51 | # %% 52 | # Load silicon dispersion from the refractiveindexinfo database 53 | # ------------------------------------------------------------- 54 | # You can load any material from the index 55 | # `refractiveindex.info `__, which is 56 | # embedded into the software (so you may use it offline, too). Here, we 57 | # are interested in the literature values for the silicon substrate. 58 | # First we need to load the database with ``rii_db = elli.db.RII()`` and 59 | # then we can query it with ``rii_db.get_mat("Si", "Aspnes")`` to load 60 | # this 61 | # `entry `__. 62 | rii_db = elli.db.RII() 63 | Si = rii_db.get_mat("Si", "Aspnes") 64 | 65 | 66 | # %% 67 | # Building the model 68 | # ------------------ 69 | # Here the model is build and the experimental structure is returned. 70 | # For details on this process please refer to the :ref:`Basic usage` example. 71 | # When executed in an jupyter notebook this displays an interactive graph 72 | # with which you can select the start parameters before fitting the data. 73 | @fit(tss, params) 74 | def model(lbda, params): 75 | SiO2 = elli.Cauchy( 76 | params["SiO2_n0"], 77 | params["SiO2_n1"], 78 | params["SiO2_n2"], 79 | params["SiO2_k0"], 80 | params["SiO2_k1"], 81 | params["SiO2_k2"], 82 | ).get_mat() 83 | TiO2 = elli.Cauchy( 84 | params["TiO2_n0"], 85 | params["TiO2_n1"], 86 | params["TiO2_n2"], 87 | params["TiO2_k0"], 88 | params["TiO2_k1"], 89 | params["TiO2_k2"], 90 | ).get_mat() 91 | 92 | Layer = [elli.Layer(TiO2, params["TiO2_d"]), elli.Layer(SiO2, params["SiO2_d"])] 93 | 94 | return elli.Structure(elli.AIR, Layer, Si).evaluate(lbda, 70, solver=elli.Solver2x2) 95 | # Alternative: Use 4x4 Solver with scipy propagator 96 | # return elli.Structure(elli.AIR, Layer, Si).evaluate(lbda, 70, solver=elli.Solver4x4, propagator=elli.PropagatorExpm()) 97 | 98 | 99 | # %% 100 | # Plot & Fit model 101 | # ---------------- 102 | # We plot the model to see the deviation with the initial parameters. 103 | model.plot() 104 | 105 | 106 | # %% 107 | # Now lets perform the fit and plot the comparison of 108 | # calculation and experimental data afterwards. 109 | fit_stats = model.fit() 110 | model.plot() 111 | 112 | # %% 113 | # We can also have a look at the fit statistics. 114 | fit_stats 115 | 116 | # %% 117 | # References 118 | # ---------- 119 | # `Here `_ 120 | # you can find the latest jupyter notebook and data files of this example. 121 | -------------------------------------------------------------------------------- /examples/gallery/plot_03_custom_fitting.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom fitting example 3 | ====================== 4 | 5 | This is a short example for fitting data, which is not covered by the fitting widget. 6 | It relies on calling lmfit manually. Therefore one needs to provide a fitting function, which calculates the numerical residual between measurement and model. 7 | This notebook is about fitting multiple datasets from different angles of incidents simultaneously, but can be adapted to a wide range of different use cases. 8 | 9 | """ 10 | 11 | # %% 12 | import numpy as np 13 | import elli 14 | from elli.fitting import ParamsHist 15 | from lmfit import minimize, fit_report 16 | import matplotlib.pyplot as plt 17 | 18 | # sphinx_gallery_thumbnail_path = '_static/custom_fitting.png' 19 | 20 | 21 | # %% 22 | # Data import 23 | # ----------- 24 | data = elli.read_nexus_psi_delta("SiO2onSi.ellips.nxs").loc[ 25 | (slice(None), slice(210, 800)), : 26 | ] 27 | lbda = data.loc[50].index.get_level_values("Wavelength").to_numpy() 28 | data 29 | 30 | # %% 31 | # Setting up invariant materials and fitting parameters 32 | # ----------------------------------------------------- 33 | rii_db = elli.db.RII() 34 | Si = rii_db.get_mat("Si", "Aspnes") 35 | 36 | params = ParamsHist() 37 | params.add("SiO2_n0", value=1.452, min=-100, max=100, vary=True) 38 | params.add("SiO2_n1", value=36.0, min=-40000, max=40000, vary=True) 39 | params.add("SiO2_n2", value=0, min=-40000, max=40000, vary=False) 40 | params.add("SiO2_k0", value=0, min=-100, max=100, vary=False) 41 | params.add("SiO2_k1", value=0, min=-40000, max=40000, vary=False) 42 | params.add("SiO2_k2", value=0, min=-40000, max=40000, vary=False) 43 | params.add("SiO2_d", value=20, min=0, max=40000, vary=True) 44 | 45 | 46 | # %% 47 | # Model helper function 48 | # --------------------- 49 | # This model function is not strictly needed, but simplifies the fit function, as the model only needs to be defined once. 50 | def model(lbda, angle, params): 51 | SiO2 = elli.Cauchy( 52 | params["SiO2_n0"], 53 | params["SiO2_n1"], 54 | params["SiO2_n2"], 55 | params["SiO2_k0"], 56 | params["SiO2_k1"], 57 | params["SiO2_k2"], 58 | ).get_mat() 59 | 60 | structure = elli.Structure( 61 | elli.AIR, 62 | [elli.Layer(SiO2, params["SiO2_d"])], 63 | Si, 64 | ) 65 | 66 | return structure.evaluate(lbda, angle, solver=elli.Solver2x2) 67 | 68 | 69 | # %% 70 | # Defining the fit function 71 | # ------------------------- 72 | # The fit function follows the protocol defined by the lmfit package and needs the parameters dictionary as first argument. 73 | # It has to return a residual value, which will be minimized. Here psi and delta are used to calculate the residual, but could be changed to transmission or reflection data. 74 | 75 | 76 | def fit_function(params, lbda, data): 77 | residual = [] 78 | 79 | for phi_i in [50, 60, 70]: 80 | model_result = model(lbda, phi_i, params) 81 | 82 | resid_psi = data.loc[(phi_i, "Ψ")].to_numpy() - model_result.psi 83 | resid_delta = data.loc[(phi_i, "Δ")].to_numpy() - model_result.delta 84 | 85 | residual.append(resid_psi) 86 | residual.append(resid_delta) 87 | 88 | return np.concatenate(residual) 89 | 90 | 91 | # %% 92 | # Running the fit 93 | # --------------- 94 | # The fitting is performed by calling the minimize function with the fit_function and the needed arguments. 95 | # It is possible to change the underlying algorithm by providing the method kwarg. 96 | 97 | out = minimize(fit_function, params, args=(lbda, data), method="leastsq") 98 | print(fit_report(out)) 99 | 100 | # %% 101 | # Plotting the results 102 | # ---------------------------------------------- 103 | 104 | fit_50 = model(lbda, 50, out.params) 105 | fit_60 = model(lbda, 60, out.params) 106 | fit_70 = model(lbda, 70, out.params) 107 | 108 | fig = plt.figure(dpi=100) 109 | ax = fig.add_subplot(1, 1, 1) 110 | ax.scatter(lbda, data.loc[(50, "Ψ")], s=20, alpha=0.1, label="50° Measurement") 111 | ax.scatter(lbda, data.loc[(60, "Ψ")], s=20, alpha=0.1, label="Psi 60° Measurement") 112 | ax.scatter(lbda, data.loc[(70, "Ψ")], s=20, alpha=0.1, label="Psi 70° Measurement") 113 | (psi50,) = ax.plot(lbda, fit_50.psi, c="tab:blue", label="Psi 50°") 114 | (psi60,) = ax.plot(lbda, fit_60.psi, c="tab:orange", label="Psi 60°") 115 | (psi70,) = ax.plot(lbda, fit_70.psi, c="tab:green", label="Psi 70°") 116 | ax.set_xlabel("wavelenth / nm") 117 | ax.set_ylabel("psi / degree") 118 | ax.legend(handles=[psi50, psi60, psi70], loc="lower left") 119 | fig.canvas.draw() 120 | 121 | fig = plt.figure(dpi=100) 122 | ax = fig.add_subplot(1, 1, 1) 123 | ax.scatter(lbda, data.loc[(50, "Δ")], s=20, alpha=0.1, label="Delta 50° Measurement") 124 | ax.scatter(lbda, data.loc[(60, "Δ")], s=20, alpha=0.1, label="Delta 60° Measurement") 125 | ax.scatter(lbda, data.loc[(70, "Δ")], s=20, alpha=0.1, label="Delta 70° Measurement") 126 | (delta50,) = ax.plot(lbda, fit_50.delta, c="tab:blue", label="Delta 50°") 127 | (delta60,) = ax.plot(lbda, fit_60.delta, c="tab:orange", label="Delta 60°") 128 | (delta70,) = ax.plot(lbda, fit_70.delta, c="tab:green", label="Delta 70°") 129 | ax.set_xlabel("wavelenth / nm") 130 | ax.set_ylabel("delta / degree") 131 | ax.legend(handles=[delta50, delta60, delta70], loc="lower right") 132 | fig.canvas.draw() 133 | -------------------------------------------------------------------------------- /examples/gallery/plot_SiO2_Si_MM.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mueller matrix 3 | ============== 4 | 5 | Mueller matrix fit to a SiO2 on Si measurement. 6 | """ 7 | 8 | # %% 9 | import elli 10 | from elli.fitting import ParamsHist, fit_mueller_matrix 11 | 12 | # sphinx_gallery_thumbnail_path = '_static/mueller_matrix.png' 13 | 14 | 15 | # %% 16 | # Read data 17 | # --------- 18 | # 19 | # We load the data from an ascii file containing each of the mueller matrix elements. 20 | # The wavelength range is cut to be in between 210 nm and 820 nm, 21 | # to stay in the range of the provided literature values for Si. 22 | # The data is expected to be in a pandas dataframe containing the columns Mxy, 23 | # where x and y refer to the matrix element inside the mueller matrix. 24 | # The data is scaled by the M11 element, such that :math:`M_{11} = 1` for all wavelengths. 25 | # To show the structure we print the `MM` dataframe. 26 | # If you load your data from another source make sure it adheres to this form. 27 | MM = elli.read_spectraray_mmatrix("Wafer_MM_70.txt").loc[210:820] 28 | print(MM) 29 | 30 | # %% 31 | # Setting start parameters 32 | # ------------------------ 33 | # Here we set the start parameters for the SiO2 cauchy dispersion 34 | # and thickness of the layer. 35 | params = ParamsHist() 36 | params.add("SiO2_n0", value=1.452, min=-100, max=100, vary=True) 37 | params.add("SiO2_n1", value=36.0, min=-40000, max=40000, vary=True) 38 | params.add("SiO2_n2", value=0, min=-40000, max=40000, vary=True) 39 | params.add("SiO2_k0", value=0, min=-100, max=100, vary=True) 40 | params.add("SiO2_k1", value=0, min=-40000, max=40000, vary=True) 41 | params.add("SiO2_k2", value=0, min=-40000, max=40000, vary=True) 42 | params.add("SiO2_d", value=120, min=0, max=40000, vary=True) 43 | 44 | # %% 45 | # Load silicon dispersion from the refractiveindexinfo database 46 | # ------------------------------------------------------------- 47 | # You can load any material from the index 48 | # `refractiveindex.info `__, which is 49 | # embedded into the software (so you may use it offline, too). Here, we 50 | # are interested in the literature values for the silicon substrate. 51 | # First we need to load the database with ``rii_db = elli.db.RII()`` and 52 | # then we can query it with ``rii_db.get_mat("Si", "Aspnes")`` to load 53 | # this 54 | # `entry `__. 55 | rii_db = elli.db.RII() 56 | Si = rii_db.get_mat("Si", "Aspnes") 57 | 58 | 59 | # %% 60 | # Building the model 61 | # ------------------ 62 | # Here the model is build and the experimental structure is returned. 63 | # For details on this process please refer to the :ref:`Basic usage` example. 64 | # When executed in an jupyter notebook this displays an interactive graph 65 | # with which you can select the start parameters before fitting the data. 66 | @fit_mueller_matrix(MM, params, display_single=False, sharex=True, full_scale=False) 67 | def model(lbda, params): 68 | SiO2 = elli.Cauchy( 69 | params["SiO2_n0"], 70 | params["SiO2_n1"], 71 | params["SiO2_n2"], 72 | params["SiO2_k0"], 73 | params["SiO2_k1"], 74 | params["SiO2_k2"], 75 | ).get_mat() 76 | 77 | Layer = [elli.Layer(SiO2, params["SiO2_d"])] 78 | 79 | return elli.Structure(elli.AIR, Layer, Si).evaluate( 80 | lbda, 70, solver=elli.Solver4x4, propagator=elli.PropagatorExpm() 81 | ) 82 | 83 | 84 | # %% 85 | # Plot & Fit the model 86 | # -------------------- 87 | # Here we plot the model at the initial parameter set vs. the experimental data. 88 | model.plot() 89 | 90 | # %% 91 | # We can also plot the residual between measurement and model. 92 | model.plot_residual() 93 | 94 | # %% 95 | # Now we execute a fit and plot the model afterwards. 96 | fit_stats = model.fit() 97 | model.plot(full_scale=False) 98 | 99 | # %% 100 | # For comparison we plot the residual again to have a figure of merit 101 | # for the fit quality 102 | model.plot_residual() 103 | 104 | # %% 105 | # We may also print the fit statistics. 106 | fit_stats 107 | 108 | # %% 109 | # References 110 | # ---------- 111 | # `Here `_ 112 | # you can find the latest jupyter notebook and data files of this example. 113 | -------------------------------------------------------------------------------- /examples/gallery/plot_bragg_mirror.py: -------------------------------------------------------------------------------- 1 | """ 2 | TiO2/SiO2 Bragg mirror 3 | ====================== 4 | """ 5 | 6 | # %% 7 | # Example of a TiO2/SiO2 Bragg mirror with 8.5 periods 8 | # ---------------------------------------------------- 9 | # 10 | # Authors: O. Castany, M.Müller 11 | 12 | # %% 13 | import elli 14 | import elli.plot as elliplot 15 | import matplotlib.pyplot as plt 16 | import numpy as np 17 | 18 | np.set_printoptions(suppress=True, precision=3) 19 | 20 | # %% 21 | # Material definition 22 | # ------------------- 23 | # We define air as incidence material and glass as exit material. 24 | # SiO2 and TiO2 are defined by simplified dispersion relations. 25 | air = elli.AIR 26 | glass = elli.ConstantRefractiveIndex(1.5).get_mat() 27 | 28 | n_SiO2 = 1.47 29 | n_TiO2 = 2.23 + 1j * 5.2e-4 30 | 31 | SiO2 = elli.ConstantRefractiveIndex(n_SiO2).get_mat() 32 | TiO2 = elli.ConstantRefractiveIndex(n_TiO2).get_mat() 33 | 34 | # %% 35 | # Create layers and structure 36 | # --------------------------- 37 | # The SiO2 and TiO2 layers are set to the thickness of an 38 | # quarterwaveplate of the respective material at 1550 nm. 39 | # 40 | # The layers are then stacked alternatingly and put into the 41 | # complete structure with air and the glass substrate. 42 | lbda0 = 1550 43 | 44 | d_SiO2 = elli.get_qwp_thickness(SiO2, lbda0) 45 | d_TiO2 = elli.get_qwp_thickness(TiO2, lbda0) 46 | 47 | print("Thickness of the SiO2 QWP: {} nm".format(d_SiO2)) 48 | print("Thickness of the TiO2 QWP: {} nm".format(d_TiO2)) 49 | 50 | L_SiO2 = elli.Layer(SiO2, d_SiO2) 51 | L_TiO2 = elli.Layer(TiO2, d_TiO2) 52 | 53 | # Repeated layers: 8.5 periods 54 | layerstack = elli.RepeatedLayers([L_TiO2, L_SiO2], 8, 0, 1) 55 | 56 | s = elli.Structure(air, [layerstack], glass) 57 | 58 | # %% 59 | # Calculation 60 | # ----------- 61 | (lbda1, lbda2) = (1100, 2500) 62 | lbda_list = np.linspace(lbda1, lbda2, 200) 63 | 64 | data = s.evaluate(lbda_list, 0) 65 | 66 | R = data.R 67 | T = data.T 68 | 69 | 70 | # %% 71 | # Structure Graph 72 | # --------------- 73 | # Schema of the variation of the refractive index in z-direction. 74 | elliplot.draw_structure(s) 75 | 76 | # %% 77 | # Reflection and Transmission Graph 78 | # --------------------------------- 79 | fig = plt.figure() 80 | ax = fig.add_subplot(1, 1, 1) 81 | ax.plot(lbda_list, T, label="$T$") 82 | ax.plot(lbda_list, R, label="$R$") 83 | ax.legend(loc="center right") 84 | ax.set_xlabel(r"Wavelength $\lambda$ (nm)") 85 | ax.set_ylabel(r"Power reflection $R$ or transmission $T$") 86 | ax.set_title(r"Bragg mirror: Air/{TiO$_2$/SiO$_2$}x8/TiO$_2$/Glass") 87 | plt.show() 88 | -------------------------------------------------------------------------------- /examples/gallery/plot_cholesteric_lq.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cholesteric Liquid Crystal 3 | ========================== 4 | """ 5 | 6 | # %% 7 | # Example of a cholesteric liquid crystal 8 | # --------------------------------------- 9 | # Authors: O. Castany, C. Molinaro, M. Müller 10 | 11 | # %% 12 | import elli 13 | import elli.plot as elliplot 14 | import matplotlib.pyplot as plt 15 | import numpy as np 16 | from scipy.constants import c, pi 17 | 18 | # %% 19 | # Setup materials and structure 20 | # ----------------------------- 21 | glass = elli.IsotropicMaterial(elli.ConstantRefractiveIndex(1.55)) 22 | front = back = glass 23 | 24 | # Liquid crystal oriented along the x direction 25 | (no, ne) = (1.5, 1.7) 26 | Dn = ne - no 27 | n_med = (ne + no) / 2 28 | LC = elli.UniaxialMaterial( 29 | elli.ConstantRefractiveIndex(no), elli.ConstantRefractiveIndex(ne) 30 | ) # ne is along z 31 | R = elli.rotation_v_theta(elli.E_Y, 90) # rotation of pi/2 along y 32 | LC.set_rotation(R) # apply rotation from z to x 33 | 34 | # Cholesteric pitch (nm): 35 | p = 650 36 | 37 | # One half turn of a right-handed helix: 38 | TN = elli.TwistedLayer(LC, p / 2, angle=180, div=35) 39 | 40 | # Repetition the helix layer 41 | N = 15 # number half pitch repetitions 42 | h = N * p / 2 43 | L = elli.RepeatedLayers([TN], N) 44 | s = elli.Structure(front, [L], back) 45 | 46 | # Calculation parameters 47 | lbda_min, lbda_max = 800, 1200 # (nm) 48 | lbda_B = p * n_med 49 | lbda_list = np.linspace(lbda_min, lbda_max, 100) 50 | 51 | # %% 52 | # Analytical calculation for the maximal reflection 53 | # ------------------------------------------------- 54 | R_th = np.tanh(Dn / n_med * pi * h / p) ** 2 55 | lbda_B1, lbda_B2 = p * no, p * ne 56 | 57 | # %% 58 | # Calculation with pyElli 59 | # ----------------------- 60 | data = s.evaluate(lbda_list, 0) 61 | 62 | T_pp = data.T_pp 63 | T_ps = data.T_ps 64 | T_ss = data.T_ss 65 | T_sp = data.T_sp 66 | 67 | # Transmission coefficients for incident unpolarized light: 68 | T_pn = 0.5 * (T_pp + T_ps) 69 | T_sn = 0.5 * (T_sp + T_ss) 70 | T_nn = T_sn + T_pn 71 | 72 | # Transmission coefficients for 's' and 'p' polarized light, with 73 | # unpolarized measurement. 74 | T_ns = T_ps + T_ss 75 | T_np = T_pp + T_sp 76 | 77 | # Right-circular wave is reflected in the stop-band. 78 | # R_LR, T_LR close to zero. 79 | R_RR = data.Rc_RR 80 | R_LR = data.Rc_LR 81 | T_RR = data.Tc_RR 82 | T_LR = data.Tc_LR 83 | 84 | # Left-circular wave is transmitted in the full spectrum. 85 | # T_RL, R_RL, R_LL close to zero, T_LL close to 1. 86 | T_LL = data.Tc_LL 87 | R_LL = data.Rc_LL 88 | 89 | # %% 90 | # Plotting 91 | # -------- 92 | fig = plt.figure() 93 | ax = fig.add_subplot(1, 1, 1) 94 | 95 | # Draw rectangle for λ ∈ [p·no, p·ne], and T ∈ [0, R_th] 96 | rectangle = plt.Rectangle((lbda_B1, 0), lbda_B2 - lbda_B1, R_th, color="cyan") 97 | ax.add_patch(rectangle) 98 | 99 | ax.plot(lbda_list, R_RR, "--", label="R_RR") 100 | ax.plot(lbda_list, T_RR, label="T_RR") 101 | ax.plot(lbda_list, T_nn, label="T_nn") 102 | ax.plot(lbda_list, T_ns, label="T_ns") 103 | ax.plot(lbda_list, T_np, label="T_np") 104 | 105 | ax.legend(loc="center right", bbox_to_anchor=(1.00, 0.50)) 106 | 107 | ax.set_title( 108 | "Right-handed Cholesteric Liquid Crystal, aligned along \n" 109 | + "the $x$ direction, with {:.1f} helix pitches.".format(N / 2.0) 110 | ) 111 | ax.set_xlabel(r"Wavelength $\lambda_0$ (nm)") 112 | ax.set_ylabel(r"Power transmission $T$ and reflexion $R$") 113 | plt.show() 114 | 115 | elliplot.draw_structure(s) 116 | -------------------------------------------------------------------------------- /examples/gallery/plot_interface_reflection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interface reflection 3 | ===================== 4 | """ 5 | 6 | # %% 7 | # Interface between two materials 8 | # ------------------------------- 9 | # Authors: O. Castany, C. Molinaro, M. Müller 10 | # 11 | # Interface between two materials n1/n2. 12 | # Calculations of the transmission and reflexion coefficients with varying incidence angle. 13 | import elli 14 | import matplotlib.pyplot as plt 15 | import numpy as np 16 | from scipy.constants import c, pi 17 | 18 | # %% 19 | # Structure definition 20 | # ------------------------ 21 | n1 = 1 22 | n2 = 1.5 23 | front = elli.IsotropicMaterial(elli.ConstantRefractiveIndex(n1)) 24 | back = elli.IsotropicMaterial(elli.ConstantRefractiveIndex(n2)) 25 | 26 | # Structure 27 | s = elli.Structure(front, [], back) 28 | 29 | # Parameters for the calculation 30 | lbda = 1000 31 | k0 = 2 * pi / lbda 32 | Phi_list = np.linspace(0, 89, 90) #  range for the incidence angles 33 | 34 | # %% 35 | # Analytical calculation 36 | # ------------------------ 37 | Phi_i = np.deg2rad(Phi_list) 38 | 39 | Phi_t = np.arcsin((n1 * np.sin(Phi_i) / n2).astype(complex)) 40 | kz1 = n1 * k0 * np.cos(Phi_i) 41 | kz2 = n2 * k0 * np.cos(Phi_t) 42 | r_s = (kz1 - kz2) / (kz1 + kz2) 43 | t_s = 1 + r_s 44 | r_p = (kz1 * n2**2 - kz2 * n1**2) / (kz1 * n2**2 + kz2 * n1**2) 45 | t_p = np.cos(Phi_i) * (1 - r_p) / np.cos(Phi_t) 46 | 47 | # Reflection and transmission coefficients, polarisation s and p 48 | R_th_ss = abs(r_s) ** 2 49 | R_th_pp = abs(r_p) ** 2 50 | t2_th_ss = abs(t_s) ** 2 51 | t2_th_pp = abs(t_p) ** 2 52 | # The power transmission coefficient is T = Re(kz2/kz1) × |t|^2 53 | correction = np.real(kz2 / kz1) 54 | T_th_ss = correction * t2_th_ss 55 | T_th_pp = correction * t2_th_pp 56 | 57 | 58 | # %% 59 | # Calculation with pyElli 60 | # ----------------------- 61 | data = elli.ResultList([s.evaluate(lbda, Phi_i) for Phi_i in Phi_list]) 62 | 63 | R_pp = data.R_pp 64 | R_ss = data.R_ss 65 | 66 | T_pp = data.T_pp 67 | T_ss = data.T_ss 68 | 69 | t2_pp = np.abs(data.t_pp) ** 2 70 | t2_ss = np.abs(data.t_ss) ** 2 71 | 72 | # %% 73 | # Plotting 74 | # ---------- 75 | fig = plt.figure(figsize=(12.0, 6.0)) 76 | plt.rcParams["axes.prop_cycle"] = plt.cycler("color", "bgrcmk") 77 | ax = fig.add_axes([0.1, 0.1, 0.7, 0.8]) 78 | 79 | d = np.vstack((R_ss, R_pp, t2_ss, t2_pp, T_ss, T_pp)).T 80 | lines1 = ax.plot(Phi_list, d) 81 | legend1 = ("R_ss", "R_pp", "t2_ss", "t2_pp", "T_ss", "T_pp") 82 | 83 | d = np.vstack((R_th_ss, R_th_pp, t2_th_ss, t2_th_pp, T_th_ss, T_th_pp)).T 84 | lines2 = ax.plot(Phi_list, d, ".") 85 | legend2 = ("R_th_ss", "R_th_pp", "t2_th_ss", "t2_th_pp", "T_th_ss", "T_th_pp") 86 | 87 | ax.legend( 88 | lines1 + lines2, 89 | legend1 + legend2, 90 | loc="upper left", 91 | bbox_to_anchor=(1.05, 1), 92 | borderaxespad=0.0, 93 | ) 94 | 95 | ax.set_title("Interface n$_1$={:} / n$_2$={:}".format(n1, n2)) 96 | ax.set_xlabel(r"Incidence angle $\Phi_i$ ") 97 | ax.set_ylabel(r"Reflexion and transmission coefficients $R$, $T$, $|t|^2$") 98 | 99 | plt.show() 100 | 101 | # %% 102 | -------------------------------------------------------------------------------- /logo/logo_bw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 43 | 44 | 46 | 49 | 53 | 61 | 62 | 63 | 64 | 69 | 74 | 81 | 88 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /logo/logo_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 43 | 44 | 46 | 49 | 53 | 61 | 62 | 63 | 64 | 69 | 74 | 81 | 88 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /logo/logo_gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 43 | 44 | 46 | 49 | 53 | 61 | 62 | 63 | 64 | 69 | 74 | 81 | 88 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /logo/logo_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 43 | 44 | 46 | 49 | 53 | 61 | 62 | 63 | 64 | 69 | 74 | 81 | 88 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=64.0.1", "setuptools-scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pyElli" 7 | description = "An ellipsometry analysis tool for reproducible and comprehensible building of optical models." 8 | dynamic = ["version"] 9 | authors = [ 10 | { name = "Marius Müller", email = "marius.mueller@tutanota.de" }, 11 | { name = "Florian Dobener", email = "pyelli@schroedingerscat.org" }, 12 | ] 13 | requires-python = ">=3.9" 14 | license = { file = "LICENSE.txt" } 15 | readme = "README.md" 16 | classifiers = [ 17 | "Development Status :: 4 - Beta", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | ] 24 | dependencies = [ 25 | "scipy", 26 | "numpy>=1.20", 27 | "numpy<2.0.0;python_version<'3.10'", 28 | "pandas>=1.0.0", 29 | "h5py", 30 | "pyyaml", 31 | "importlib-resources", 32 | "rapidfuzz", 33 | "lark>=1.1.5", 34 | "pint", 35 | "chardet", 36 | ] 37 | 38 | [project.optional-dependencies] 39 | fitting = [ 40 | "ipython", 41 | "ipywidgets", 42 | "anywidget", 43 | "plotly<6", 44 | "matplotlib", 45 | "lmfit", 46 | ] 47 | dev = [ 48 | "pytest", 49 | "pytest-benchmark", 50 | "pytest-cov", 51 | "nbmake", 52 | "ruff==0.8.5", 53 | "uv", 54 | "pre-commit", 55 | ] 56 | docs = [ 57 | "sphinx!=5.1.0", 58 | "sphinx_rtd_theme", 59 | "sphinx-mdinclude", 60 | "sphinx-gallery", 61 | "sphinx-plotly-directive", 62 | "sphinxcontrib-mermaid", 63 | "myst_parser", 64 | ] 65 | 66 | [project.urls] 67 | homepage = "https://github.com/PyEllips/pyElli/" 68 | repository = "https://github.com/PyEllips/pyElli/" 69 | documentation = "https://pyelli.readthedocs.io/en/latest/" 70 | changelog = "https://github.com/PyEllips/pyElli/blob/master/CHANGELOG.md" 71 | tracker = "https://github.com/PyEllips/pyElli/issues" 72 | download = "https://github.com/PyEllips/pyElli/releases" 73 | 74 | [tool.setuptools_scm] 75 | version_scheme = "python-simplified-semver" 76 | local_scheme = "node-and-date" 77 | 78 | [tool.setuptools.packages.find] 79 | where = ["src"] 80 | exclude = ["tests*"] 81 | 82 | [tool.ruff] 83 | include = ["src/*.py", "tests/*.py"] 84 | exclude = ["src/elli/database/refractiveindexinfo-database/*.py"] 85 | line-length = 88 86 | indent-width = 4 87 | 88 | [tool.ruff.lint] 89 | select = [ 90 | "E", # pycodestyle 91 | "W", # pycodestyle 92 | "PL", # pylint 93 | "NPY201", # numpy 94 | ] 95 | ignore = [ 96 | "E501", # Line too long ({width} > {limit} characters) 97 | "E701", # Multiple statements on one line (colon) 98 | "E731", # Do not assign a lambda expression, use a def 99 | "E402", # Module level import not at top of file 100 | "PLR0911", # Too many return statements 101 | "PLR0912", # Too many branches 102 | "PLR0913", # Too many arguments in function definition 103 | "PLR0915", # Too many statements 104 | "PLR2004", # Magic value used instead of constant 105 | "PLW0603", # Using the global statement 106 | "PLW2901", # redefined-loop-name 107 | "PLR1714", # consider-using-in 108 | "PLR5501", # else-if-used 109 | "PLC2401", # temporary: Non-ASCII variable names 110 | ] 111 | fixable = ["ALL"] 112 | 113 | [tool.ruff.format] 114 | quote-style = "double" 115 | indent-style = "space" 116 | skip-magic-trailing-comma = false 117 | line-ending = "auto" 118 | 119 | [tool.pytest.ini_options] 120 | testpaths = ["tests", "examples"] 121 | -------------------------------------------------------------------------------- /scripts/generate_ipynb_from_gallery.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd examples/gallery 4 | 5 | # Copy Basic usage example 6 | sphx_glr_python_to_jupyter.py plot_01_basic_usage.py 7 | mv plot_01_basic_usage.ipynb ../Basic\ Usage/Basic\ Usage.ipynb 8 | cp SiO2onSi.ellips.nxs ../Basic\ Usage/ 9 | 10 | # Copy SiO2 Si Müller Matrix example 11 | sphx_glr_python_to_jupyter.py plot_SiO2_Si_MM.py 12 | mv plot_SiO2_Si_MM.ipynb ../SiO2_Si\ Mueller\ Matrix/SiO2_Si\ Mueller\ Matrix.ipynb 13 | cp Wafer_MM_70.txt ../SiO2_Si\ Mueller\ Matrix/ 14 | 15 | # Copy TiO2 Multilayer example 16 | sphx_glr_python_to_jupyter.py plot_02_TiO2_multilayer.py 17 | mv plot_02_TiO2_multilayer.ipynb ../TiO2\ Fit/TiO2\ Multilayerfit.ipynb 18 | cp TiO2_400cycles.txt ../TiO2\ Fit/ 19 | 20 | # Copy Bragg-Mirror 21 | sphx_glr_python_to_jupyter.py plot_bragg_mirror.py 22 | mv plot_bragg_mirror.ipynb ../Bragg-mirror/Bragg-Mirror.ipynb 23 | 24 | # Copy Interface Reflection 25 | sphx_glr_python_to_jupyter.py plot_interface_reflection.py 26 | mv plot_interface_reflection.ipynb ../Interfaces/interface-reflection.ipynb 27 | 28 | # Copy Cholesteric Liquid 29 | sphx_glr_python_to_jupyter.py plot_cholesteric_lq.py 30 | mv plot_cholesteric_lq.ipynb ../Liquid\ crystals/cholesteric-liquid.ipynb 31 | -------------------------------------------------------------------------------- /scripts/generate_requirements.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | uv pip compile --generate-hashes --upgrade --output-file=requirements/requirements.txt pyproject.toml 4 | 5 | uv pip compile --extra=fitting --generate-hashes --upgrade --output-file=requirements/fitting-requirements.txt \ 6 | requirements/requirements.txt pyproject.toml 7 | 8 | uv pip compile --extra=fitting --extra=dev --generate-hashes --upgrade\ 9 | --output-file=requirements/dev-requirements.txt \ 10 | requirements/fitting-requirements.txt \ 11 | pyproject.toml 12 | 13 | uv pip compile --extra=fitting --extra=dev --extra=docs --generate-hashes --upgrade\ 14 | --output-file=docs/requirements.txt \ 15 | requirements/dev-requirements.txt \ 16 | pyproject.toml 17 | -------------------------------------------------------------------------------- /src/elli/__init__.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | import sys 3 | 4 | from . import database as db 5 | from .database.materials_db import AIR 6 | from .dispersions import * 7 | from .dispersions.base_dispersion import * 8 | from .experiment import Experiment 9 | from .importer.accurion import read_accurion_psi_delta 10 | from .importer.nexus import * 11 | from .importer.spectraray import * 12 | from .importer.woollam import read_woollam_psi_delta, read_woollam_rho, scale_to_nm 13 | from .materials import * 14 | from .result import Result, ResultList 15 | from .solver2x2 import Solver2x2 16 | from .solver4x4 import * 17 | from .structure import * 18 | from .utils import * 19 | -------------------------------------------------------------------------------- /src/elli/database/__init__.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | from .refractive_index_info import RII 3 | -------------------------------------------------------------------------------- /src/elli/database/materials_db.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | from ..dispersions import ConstantRefractiveIndex 3 | from ..materials import IsotropicMaterial 4 | 5 | AIR = IsotropicMaterial(ConstantRefractiveIndex(n=1)) 6 | -------------------------------------------------------------------------------- /src/elli/dispersions/__init__.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | from .cauchy import Cauchy 3 | from .cauchy_custom import CauchyCustomExponent 4 | from .cauchy_urbach import CauchyUrbach 5 | from .constant_refractive_index import ConstantRefractiveIndex 6 | from .drude_energy import DrudeEnergy 7 | from .drude_resistivity import DrudeResistivity 8 | from .epsilon_inf import EpsilonInf 9 | from .gaussian import Gaussian 10 | from .lorentz_energy import LorentzEnergy 11 | from .lorentz_lambda import LorentzLambda 12 | from .poles import Poles 13 | from .polynomial import Polynomial 14 | from .sellmeier import Sellmeier 15 | from .sellmeier_custom import SellmeierCustomExponent 16 | from .table_epsilon import TableEpsilon 17 | from .table_index import Table 18 | from .table_spectraray import TableSpectraRay 19 | from .tanguy import Tanguy 20 | from .tauc_lorentz import TaucLorentz 21 | from .cody_lorentz import CodyLorentz 22 | from .pseudo_dielectric import PseudoDielectricFunction 23 | from .formula import Formula, FormulaIndex 24 | -------------------------------------------------------------------------------- /src/elli/dispersions/cauchy.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Cauchy dispersion.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from .base_dispersion import IndexDispersion 7 | 8 | 9 | class Cauchy(IndexDispersion): 10 | r"""Cauchy dispersion. 11 | 12 | Single parameters: 13 | :n0: Defaults to 1.5. 14 | :n1: Defaults to 0. Unit in nm\ :sup:`2`. 15 | :n2: Defaults to 0. Unit in nm\ :sup:`4`. 16 | :k0: Defaults to 0. 17 | :k1: Defaults to 0. Unit in nm\ :sup:`2`. 18 | :k2: Defaults to 0. Unit in nm\ :sup:`4`. 19 | 20 | Repeated parameters: 21 | -- 22 | 23 | Output: 24 | .. math:: 25 | \varepsilon^{1/2}(\lambda) = 26 | \boldsymbol{n_0} + 100 \boldsymbol{n_1}/\lambda^2 + 27 | 10^7 \boldsymbol{n_2}/\lambda^4 28 | + i (\boldsymbol{k_0} + 100 \boldsymbol{k_1}/\lambda^2 29 | + 10^7 \boldsymbol{k_2}/\lambda^4) 30 | """ 31 | 32 | single_params_template = {"n0": 1.5, "n1": 0, "n2": 0, "k0": 0, "k1": 0, "k2": 0} 33 | rep_params_template = {} 34 | 35 | def refractive_index(self, lbda: npt.ArrayLike) -> npt.NDArray: 36 | return ( 37 | self.single_params.get("n0") 38 | + 1e2 * self.single_params.get("n1") / lbda**2 39 | + 1e7 * self.single_params.get("n2") / lbda**4 40 | + 1j 41 | * ( 42 | self.single_params.get("k0") 43 | + 1e2 * self.single_params.get("k1") / lbda**2 44 | + 1e7 * self.single_params.get("k2") / lbda**4 45 | ) 46 | ) 47 | -------------------------------------------------------------------------------- /src/elli/dispersions/cauchy_custom.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Cauchy dispersion with custom exponents.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from .base_dispersion import IndexDispersion 7 | 8 | 9 | class CauchyCustomExponent(IndexDispersion): 10 | r"""Cauchy dispersion with custom exponents. 11 | 12 | Single parameters: 13 | :n0: Defaults to 1.5. 14 | 15 | Repeated parameters: 16 | :f: Defaults to 0. 17 | :e: Defaults to 1. 18 | 19 | Output: 20 | .. math:: 21 | \varepsilon^{1/2}(\lambda) = 22 | \boldsymbol{n_0} + \sum_j \boldsymbol{f}_j \cdot \lambda^{\boldsymbol{e}_j} 23 | """ 24 | 25 | single_params_template = {"n0": 1.5} 26 | rep_params_template = {"f": 0, "e": 1} 27 | 28 | def refractive_index(self, lbda: npt.ArrayLike) -> npt.NDArray: 29 | return self.single_params.get("n0") + sum( 30 | c.get("f") * lbda ** c.get("e") for c in self.rep_params 31 | ) 32 | -------------------------------------------------------------------------------- /src/elli/dispersions/cauchy_urbach.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Cauchy dispersion, with Urbach tail.""" 3 | 4 | import numpy as np 5 | import numpy.typing as npt 6 | 7 | from .base_dispersion import IndexDispersion 8 | from ..utils import conversion_wavelength_energy 9 | 10 | 11 | class CauchyUrbach(IndexDispersion): 12 | r"""Cauchy dispersion, with an Urbach Tail absorption. 13 | 14 | Single parameters: 15 | :n0: Defaults to 1.5. 16 | :B: Defaults to 0. Unit in 1/eV\ :sup:`2`. 17 | :C: Defaults to 0. Unit in 1/eV\ :sup:`4`. 18 | :D: Defaults to 0. 19 | :Eg: Defaults to 0. Unit in eV. 20 | :Eu: Defaults to 1. Unit in eV. 21 | 22 | Repeated parameters: 23 | -- 24 | 25 | Output: 26 | .. math:: 27 | n(E) = 28 | \boldsymbol{n_0} + \boldsymbol{B} E^2 + \boldsymbol{C} E^4 29 | + i \boldsymbol{D} \exp (\frac{E - \boldsymbol{E_g}}{\boldsymbol{E_u}}) 30 | 31 | References: 32 | * Fujiwara: Spectroscopic Ellipsometry: Principles and Applications, 33 | John Wiley & Sons Ltd, 2007, p. 258 34 | """ 35 | 36 | single_params_template = {"n0": 1.5, "B": 0, "C": 0, "D": 0, "Eg": 2, "Eu": 0.5} 37 | rep_params_template = {} 38 | 39 | def refractive_index(self, lbda: npt.ArrayLike) -> npt.NDArray: 40 | energy = conversion_wavelength_energy(lbda) 41 | return ( 42 | self.single_params.get("n0") 43 | + self.single_params.get("B") * energy**2 44 | + self.single_params.get("C") * energy**4 45 | + 1j 46 | * self.single_params.get("D") 47 | * np.exp( 48 | (energy - self.single_params.get("Eg")) / self.single_params.get("Eu") 49 | ) 50 | ) 51 | -------------------------------------------------------------------------------- /src/elli/dispersions/cody_lorentz.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Cody-Lorentz dispersion law. Model by Ferlauto et al.""" 3 | 4 | from typing import Dict 5 | import numpy as np 6 | import numpy.typing as npt 7 | from scipy.interpolate import interp1d 8 | 9 | from ..utils import conversion_wavelength_energy 10 | from .base_dispersion import Dispersion 11 | from ..kkr import im2re_reciprocal 12 | 13 | 14 | class CodyLorentz(Dispersion): 15 | """Cody-Lorentz dispersion law. Model by Ferlauto et al. 16 | 17 | Single parameters: 18 | :Eg: Bandgap energy (eV). Defaults to 1.6. 19 | :A: Amplitude (eV). Defaults to 100. 20 | :Et: Energy at which the Urbach tail starts (eV). Defaults to 1.8. 21 | :gamma: Broadening (eV). Defaults to 2.4. 22 | :Ep: 23 | Distance from bandgap for transition from Cody type absorption 24 | to Lorentz type absorption (eV). Defaults to 0.8. 25 | :E0: Lorentz resonance energy (eV). Defaults to 3.6. 26 | :Eu: Exponential decay of the Urbach tail (eV). Defaults to 0.05. 27 | 28 | Repeated parameters: 29 | -- 30 | 31 | Output: 32 | The Cody-Lorentz dispersion. Please refer to the references for a full formula. 33 | 34 | References: 35 | * Ferlauto et al., J. Appl. Phys. 92, 2424 (2002) 36 | """ 37 | 38 | single_params_template = { 39 | "Eg": 1.6, 40 | "A": 100, 41 | "Et": 1.8, 42 | "gamma": 2.4, 43 | "Ep": 0.8, 44 | "E0": 3.6, 45 | "Eu": 0.05, 46 | } 47 | rep_params_template: Dict[str, float] = {} 48 | 49 | @staticmethod 50 | def eps2(E, Eg, A, Et, gamma, Ep, E0, Eu): 51 | """The imaginary part of the cody lorentz dispersion""" 52 | 53 | # pylint: disable=invalid-name 54 | def G(E): 55 | return (E - Eg) ** 2 / ((E - Eg) ** 2 + Ep**2) 56 | 57 | def L(E): 58 | return A * E0 * gamma * E / ((E**2 - E0**2) ** 2 + gamma**2 * E**2) 59 | 60 | E1 = Et * G(Et) * L(Et) 61 | 62 | # fmt: off 63 | return ( 64 | E1 / E * np.exp((E - Et) / Eu) * np.heaviside(Et - E, 1) 65 | + G(E) * L(E) * np.heaviside(E - Et, 0) 66 | ) 67 | # fmt: on 68 | 69 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 70 | energy = conversion_wavelength_energy(lbda) 71 | 72 | lbda_broad = np.linspace(50, 10000, 1000) 73 | energy_padded = conversion_wavelength_energy(lbda_broad) 74 | eps1 = im2re_reciprocal( 75 | CodyLorentz.eps2(energy_padded, **self.single_params), lbda_broad 76 | ) 77 | eps1_interp = interp1d(lbda_broad, eps1)(lbda) 78 | 79 | return eps1_interp + 1j * CodyLorentz.eps2(energy, **self.single_params) 80 | -------------------------------------------------------------------------------- /src/elli/dispersions/constant_refractive_index.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Constant refractive index.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from .base_dispersion import IndexDispersion 7 | 8 | 9 | class ConstantRefractiveIndex(IndexDispersion): 10 | r"""Constant refractive index. 11 | 12 | Single parameters: 13 | :n: The constant value of the refractive index. Defaults to 1. 14 | 15 | Repeated parameters: 16 | -- 17 | 18 | Output: 19 | .. math:: 20 | \varepsilon(\lambda) = \boldsymbol{n}^2 21 | """ 22 | 23 | single_params_template = {"n": 1} 24 | rep_params_template = {} 25 | 26 | def refractive_index(self, _: npt.ArrayLike) -> npt.NDArray: 27 | return self.single_params.get("n") 28 | -------------------------------------------------------------------------------- /src/elli/dispersions/drude_energy.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Drude dispersion model with parameters in units of energy.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from ..utils import conversion_wavelength_energy 7 | from .base_dispersion import Dispersion 8 | 9 | 10 | class DrudeEnergy(Dispersion): 11 | r"""Drude dispersion model with parameters in units of energy. 12 | Drude models in the literature typically contain an additional epsilon infinity value. 13 | Use `EpsilonInf` to add this parameter or simply add a number, e.g. DrudeEnergy() + 2, where 14 | 2 is the value of epsilon infinity. 15 | 16 | Single parameters: 17 | :A: Amplitude of Drude oscillator. Defaults to 0. Unit in eV\ :sup:`2` 18 | :gamma: Broadening of Drude oscillator. Defaults to 0. Unit in eV. 19 | 20 | Repeated parameters: 21 | -- 22 | 23 | Output: 24 | .. math:: 25 | \varepsilon(E) 26 | = \boldsymbol{A} / (E^2 - i \cdot \boldsymbol{gamma} \cdot E) 27 | """ 28 | 29 | single_params_template = {"A": 0, "gamma": 0} 30 | rep_params_template = {} 31 | 32 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 33 | energy = conversion_wavelength_energy(lbda) 34 | return self.single_params.get("A") / ( 35 | energy**2 - 1j * self.single_params.get("gamma") * energy 36 | ) 37 | -------------------------------------------------------------------------------- /src/elli/dispersions/drude_resistivity.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Drude dispersion model with resistivity based parameters.""" 3 | 4 | import numpy as np 5 | import numpy.typing as npt 6 | import scipy.constants as sc 7 | 8 | from ..utils import conversion_wavelength_energy 9 | from .base_dispersion import Dispersion 10 | 11 | 12 | class DrudeResistivity(Dispersion): 13 | r"""Drude dispersion model with resistivity based parameters. 14 | Drude models in the literature typically contain an additional epsilon infinity value. 15 | Use `EpsilonInf` to add this parameter or simply do DrudeEnergy() + eps_inf. 16 | 17 | Single parameters: 18 | :rho_opt: Optical resistivity. Defaults to 1. Unit in Ω-cm. 19 | :tau: Mean scattering time. Defaults to 1. Unit in s. 20 | 21 | Repeated parameters: 22 | -- 23 | 24 | Output: 25 | 26 | .. math:: 27 | \varepsilon(E) = \hbar / (\varepsilon_0 \cdot 28 | \boldsymbol{rho\_opt} \cdot \boldsymbol{tau} \cdot E^2 29 | - i \cdot \hbar \cdot E) 30 | 31 | where :math:`\hbar` is the planck constant divided by :math:`2\pi` 32 | and :math:`\varepsilon_0` is the vacuum dielectric permittivity. 33 | """ 34 | 35 | single_params_template = {"rho_opt": 1, "tau": 1} 36 | rep_params_template = {} 37 | 38 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 39 | energy = conversion_wavelength_energy(lbda) 40 | hbar = sc.value("Planck constant in eV/Hz") / 2 / np.pi 41 | eps0 = sc.value("vacuum electric permittivity") * 1e-2 42 | 43 | return hbar**2 / ( 44 | eps0 45 | * self.single_params.get("rho_opt") 46 | * (self.single_params.get("tau") * energy**2 - 1j * hbar * energy) 47 | ) 48 | -------------------------------------------------------------------------------- /src/elli/dispersions/epsilon_inf.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Constant epsilon infinity.""" 3 | 4 | from typing import Any, Dict 5 | import numpy.typing as npt 6 | 7 | from .base_dispersion import Dispersion 8 | 9 | 10 | class EpsilonInf(Dispersion): 11 | r"""Constant epsilon infinity. 12 | 13 | Single parameters: 14 | :eps: Constant value for the constant epsilon. Defaults to 1. 15 | 16 | Repeated parameters: 17 | -- 18 | 19 | Output: 20 | .. math:: 21 | \varepsilon(\lambda) = \textbf{eps} 22 | """ 23 | 24 | single_params_template = {"eps": 1} 25 | rep_params_template: Dict[str, Any] = {} 26 | 27 | def dielectric_function(self, _: npt.ArrayLike) -> npt.NDArray: 28 | return self.single_params.get("eps") 29 | -------------------------------------------------------------------------------- /src/elli/dispersions/formula.py: -------------------------------------------------------------------------------- 1 | """A formula dispersion to parse dispersion values from a formula string.""" 2 | 3 | from typing import Dict, List, Optional 4 | 5 | import numpy as np 6 | import numpy.typing as npt 7 | 8 | from elli.units import ureg 9 | from elli.dispersions.base_dispersion import BaseDispersion, Dispersion, IndexDispersion 10 | from elli.formula_parser.parser import parse_formula, FormulaTransformer 11 | 12 | 13 | class FormulaParser(BaseDispersion): 14 | r"""A formula dispersion""" 15 | 16 | @property 17 | def single_params_template(self) -> dict: 18 | return self.f_single_params 19 | 20 | @property 21 | def rep_params_template(self) -> dict: 22 | return self.f_rep_params 23 | 24 | def __init__( 25 | self, 26 | formula: str, 27 | wavelength_axis_name: str, 28 | single_params: Dict[str, float], 29 | rep_params: Dict[str, npt.ArrayLike], 30 | unit: Optional[str] = None, 31 | ): 32 | self.f_single_params: Dict[str, float] = single_params 33 | self.f_axis_name: str = wavelength_axis_name 34 | rep_params_len: Optional[int] = None 35 | rep_params_sets: List[Dict[str, float]] = [] 36 | 37 | for key, values in rep_params.items(): 38 | if not isinstance(values, (np.ndarray, list)): 39 | raise ValueError( 40 | "Repeated parameters must be given as dict of lists or numpy arrays" 41 | ) 42 | for i, value in enumerate(values): 43 | if i >= len(rep_params_sets): 44 | rep_params_sets.append({}) 45 | rep_params_sets[i][key] = value 46 | 47 | if rep_params_len is None: 48 | rep_params_len = len(values) 49 | continue 50 | if len(values) != rep_params_len: 51 | raise ValueError( 52 | f"All repeated parameters must have the same length." 53 | f"Found {values} with length {len(values)}, " 54 | f"but previous length was {rep_params_len}." 55 | ) 56 | 57 | self.f_rep_params = {} 58 | if rep_params_sets: 59 | self.f_rep_params = rep_params_sets[0] 60 | super().__init__() 61 | 62 | for rep_params_set in rep_params_sets: 63 | self.add(**rep_params_set) 64 | 65 | self.rep_params_dl = {} 66 | if self.rep_params: 67 | self.rep_params_dl = { 68 | k: np.array([dic[k] for dic in self.rep_params]) 69 | for k in self.rep_params[0] 70 | } 71 | 72 | self.formula = formula 73 | 74 | self._check_repr() 75 | 76 | self._dispersion_function = self.__dispersion_function 77 | if unit is not None: 78 | self._set_unit_conversion(unit) 79 | 80 | def _set_unit_conversion(self, unit: str): 81 | quantity = ureg(unit) 82 | if quantity.check("[length]"): 83 | scaling = ureg("nm").to(unit).magnitude 84 | self._dispersion_function = lambda lbda: self.__dispersion_function( 85 | scaling * lbda 86 | ) 87 | return 88 | 89 | if quantity.check("[energy]"): 90 | scaling = ureg("nm").to(unit).magnitude 91 | self._dispersion_function = lambda lbda: self.__dispersion_function( 92 | scaling / lbda 93 | ) 94 | return 95 | 96 | raise ValueError(f"Unsupported unit `{unit}`.") 97 | 98 | def _check_repr(self): 99 | representation = parse_formula(self.formula).data 100 | 101 | if isinstance(self, FormulaIndex) and not representation == "n": 102 | raise ValueError( 103 | f"Representation `{representation}` not supported by FormulaIndex" 104 | ) 105 | 106 | if isinstance(self, Formula) and not representation == "eps": 107 | raise ValueError( 108 | f"Representation `{representation}` not supported by Formula" 109 | ) 110 | 111 | def __dispersion_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 112 | return FormulaTransformer( 113 | x_axis_name=self.f_axis_name, 114 | x_axis_values=lbda, 115 | single_params=self.single_params, 116 | repeated_params=self.rep_params_dl, 117 | ).transform(parse_formula(self.formula))[1] 118 | 119 | 120 | class Formula(Dispersion, FormulaParser): 121 | r"A formula dispersion" 122 | 123 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 124 | return self._dispersion_function(lbda) 125 | 126 | 127 | class FormulaIndex(IndexDispersion, FormulaParser): 128 | r"""A formula dispersion in refractive index formulation""" 129 | 130 | def refractive_index(self, lbda: npt.ArrayLike) -> npt.NDArray: 131 | return self._dispersion_function(lbda) 132 | -------------------------------------------------------------------------------- /src/elli/dispersions/gaussian.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Dispersion law with gaussian oscillators.""" 3 | 4 | import numpy as np 5 | import numpy.typing as npt 6 | from numpy.lib.scimath import sqrt 7 | 8 | # pylint: disable=no-name-in-module 9 | from scipy.special import dawsn 10 | 11 | from ..utils import conversion_wavelength_energy 12 | from .base_dispersion import Dispersion 13 | 14 | 15 | class Gaussian(Dispersion): 16 | r"""Dispersion law with gaussian oscillators. 17 | 18 | Single parameters: 19 | -- 20 | 21 | Repeated parameters: 22 | :A: Amplitude of the oscillator. Defaults to 1. 23 | :E: Central energy. Defaults to 1. Unit in eV. 24 | :sigma: Broadening of the Gaussian. Defaults to 1. Unit in eV. 25 | 26 | Output: 27 | 28 | .. math:: 29 | \varepsilon(E) = \sum_j & \; 2 \cdot \boldsymbol{A}_j / \sqrt{π} \cdot 30 | (D\left(2 \cdot \sqrt{2 \cdot \ln(2)} \cdot (E + \boldsymbol{E}_j) 31 | / \boldsymbol{sigma}_j\right) \\ 32 | &- D\left(2 \cdot \sqrt{2 \cdot \ln(2)} \cdot (E - \boldsymbol{E}_j) 33 | / \boldsymbol{sigma}_j\right) \\ 34 | &+ i \cdot \Bigl(\boldsymbol{A}_j \cdot \exp\left(-(4 \cdot \ln(2) \cdot 35 | (E - \boldsymbol{E}_j)/ \boldsymbol{sigma}_j\right)^2 \\ 36 | &- \boldsymbol{A}_j \cdot \exp\left(-(4 \cdot ln(2) \cdot 37 | (E + \boldsymbol{E}_j)/ \boldsymbol{sigma}_j\right)^2\Bigr) 38 | 39 | D is the 40 | `Dawson function 41 | `_. 42 | The summation index :math:`j` is the index of the respective oscillator. 43 | 44 | References: 45 | * De Sousa Meneses, Malki, Echegut, J. Non-Cryst. Solids 351, 769-776 (2006) 46 | * Peiponen, Vartiainen, Phys. Rev. B. 44, 8301 (1991) 47 | * Fujiwara, Collins, Spectroscopic Ellipsometry for Photovoltaics Volume 1, 48 | Springer International Publishing AG, 2018, p. 137 49 | """ 50 | 51 | single_params_template = {} 52 | rep_params_template = {"A": 1, "E": 1, "sigma": 1} 53 | 54 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 55 | energy = conversion_wavelength_energy(lbda) 56 | ftos = 2 * sqrt(np.log(2)) 57 | return sum( 58 | 2 59 | * c.get("A") 60 | / sqrt(np.pi) 61 | * ( 62 | dawsn(ftos * (energy + c.get("E")) / c.get("sigma")) 63 | - dawsn(ftos * (energy - c.get("E")) / c.get("sigma")) 64 | ) 65 | + 1j 66 | * ( 67 | c.get("A") 68 | * np.exp(-((ftos * (energy - c.get("E")) / c.get("sigma")) ** 2)) 69 | - c.get("A") 70 | * np.exp(-((ftos * (energy + c.get("E")) / c.get("sigma")) ** 2)) 71 | ) 72 | for c in self.rep_params 73 | ) 74 | -------------------------------------------------------------------------------- /src/elli/dispersions/lorentz_energy.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Lorentz dispersion law with parameters in units of energy.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from ..utils import conversion_wavelength_energy 7 | from .base_dispersion import Dispersion 8 | 9 | 10 | class LorentzEnergy(Dispersion): 11 | r"""Lorentz dispersion law with parameters in units of energy. 12 | 13 | Single parameters: 14 | -- 15 | 16 | Repeated parameters: 17 | :A: Amplitude of the oscillator. Defaults to 1. 18 | :E: Resonance energy. Defaults 0. Unit in eV. 19 | :gamma: Broadening of the oscillator. Defaults to 0. Unit in eV. 20 | 21 | Output: 22 | .. math:: 23 | \varepsilon(E) = 1 + \sum_j \boldsymbol{A}_j / \left(E^2-\boldsymbol{E}_j^2 24 | + i \cdot \boldsymbol{gamma}_j \cdot E\right) 25 | 26 | With :math:`j` as the index for the respective oscillator. 27 | """ 28 | 29 | single_params_template = {} 30 | rep_params_template = {"A": 1, "E": 0, "gamma": 0} 31 | 32 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 33 | energy = conversion_wavelength_energy(lbda) 34 | return 1 + sum( 35 | c.get("A") / (c.get("E") ** 2 - energy**2 - 1j * c.get("gamma") * energy) 36 | for c in self.rep_params 37 | ) 38 | -------------------------------------------------------------------------------- /src/elli/dispersions/lorentz_lambda.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Lorentz dispersion law with parameters in units of wavelengths.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from .base_dispersion import Dispersion 7 | 8 | 9 | class LorentzLambda(Dispersion): 10 | r"""Lorentz dispersion law with parameters in units of wavelengths. 11 | 12 | Single parameters: 13 | -- 14 | 15 | Repeated parameters: 16 | :A: Amplitude of the oscillator. Defaults to 1. 17 | :lambda_r: Resonance wavelength. Defaults to 0. Unit in nm. 18 | :gamma: Broadening of the oscillator. Defaults to 0. Unit in nm. 19 | 20 | Output: 21 | 22 | .. math:: 23 | \varepsilon(\lambda) = 1 + \sum_j \boldsymbol{A}_j 24 | \cdot \lambda^2 / (\lambda^2 - \boldsymbol{lambda\_r}_j^2 25 | + i \cdot \boldsymbol{gamma}_j \cdot \lambda) 26 | 27 | The summation index :math:`j` refers to the respective oscillator. 28 | """ 29 | 30 | single_params_template = {} 31 | rep_params_template = {"A": 1, "lambda_r": 0, "gamma": 0} 32 | 33 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 34 | return 1 + sum( 35 | c.get("A") 36 | * lbda**2 37 | / (lbda**2 - c.get("lambda_r") ** 2 - 1j * c.get("gamma") * lbda) 38 | for c in self.rep_params 39 | ) 40 | -------------------------------------------------------------------------------- /src/elli/dispersions/poles.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Dispersion law for an UV and IR pole.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from ..utils import conversion_wavelength_energy 7 | from .base_dispersion import Dispersion 8 | 9 | 10 | class Poles(Dispersion): 11 | r"""Dispersion law for an UV and IR pole, 12 | i.e. Lorentz oscillators outside the fitting spectral range and zero broadening. 13 | 14 | Single parameters: 15 | :A_ir: IR Pole amplitude. Defaults to 1. Unit in eV\ :sup:`2`. 16 | :A_uv: UV Pole amplitude. Defaults to 1. Unit in eV\ :sup:`2`. 17 | :E_uv: UV Pole energy. Defaults to 6. Unit in eV. 18 | 19 | Repeated parameters: 20 | -- 21 | 22 | Output: 23 | .. math:: 24 | \varepsilon(E) = \boldsymbol{A\_ir} / E^2 25 | + \boldsymbol{A\_uv} / (\boldsymbol{E\_uv}^2 - E^2) 26 | """ 27 | 28 | single_params_template = {"A_ir": 1, "A_uv": 1, "E_uv": 6} 29 | rep_params_template = {} 30 | 31 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 32 | energy = conversion_wavelength_energy(lbda) 33 | return self.single_params.get("A_ir") / energy**2 + self.single_params.get( 34 | "A_uv" 35 | ) / (self.single_params.get("E_uv") ** 2 - energy**2) 36 | -------------------------------------------------------------------------------- /src/elli/dispersions/polynomial.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Polynomial dispersion.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from .base_dispersion import Dispersion 7 | 8 | 9 | class Polynomial(Dispersion): 10 | r"""Polynomial expression for the dielectric function. 11 | 12 | Single parameters: 13 | :e0: Defaults to 1. 14 | 15 | Repeated parameters: 16 | :f: Defaults to 0. 17 | :e: Defaults to 0. 18 | 19 | Output: 20 | .. math:: 21 | \varepsilon(\lambda) = 22 | \boldsymbol{\varepsilon_0} + \sum_j \boldsymbol{f}_j \cdot \lambda^{\boldsymbol{e}_j} 23 | """ 24 | 25 | single_params_template = {"e0": 1} 26 | rep_params_template = {"f": 0, "e": 0} 27 | 28 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 29 | return self.single_params.get("e0") + sum( 30 | c.get("f") * lbda ** c.get("e") for c in self.rep_params 31 | ) 32 | -------------------------------------------------------------------------------- /src/elli/dispersions/pseudo_dielectric.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Pseudo dielectric function dispersion.""" 3 | 4 | from typing import Union 5 | import numpy as np 6 | import numpy.typing as npt 7 | from scipy.interpolate import interp1d 8 | 9 | from .base_dispersion import Dispersion, InvalidParameters 10 | 11 | 12 | class PseudoDielectricFunction(Dispersion): 13 | r"""A pseudo dielectric function generated from experimental psi/delta values. 14 | Please note that the pseudo dielectric function can lead to unphysical behaviour, such 15 | as negative refractive indices or other spurious artifacts. 16 | Additionally, this formula is only valid for a bulk absorbing material and yields wrong 17 | results for layered materials. 18 | Therefore, it is preferable to use the pseudo dielectric function only as a helper for 19 | constructing other dispersion models. 20 | 21 | Single parameters: 22 | :angle: The measurement angle in degree under which the psi/delta values where obtained. 23 | :lbda: The wavelength region of the measurement data. Units in nm. 24 | :psi: The psi values of the measurement. Units in degree. 25 | :delta: The delta values of the measurement. Units in degree. 26 | 27 | Repeated parameters: 28 | -- 29 | 30 | Output: 31 | .. math:: 32 | \varepsilon(\lambda) = \sin^2 \left( \Theta \right) \cdot 33 | \left( 1 + \tan^2 (\Theta) \left( \frac{1 - \rho}{1 + \rho} \right) \right) 34 | 35 | With 36 | 37 | .. math:: 38 | \rho = \tan(\Psi) \cdot \exp (-i \Delta) 39 | 40 | :math:`\Theta` is the angle of incidence. 41 | """ 42 | 43 | single_params_template = {"angle": None, "lbda": None, "psi": None, "delta": None} 44 | rep_params_template = {} 45 | 46 | def __init__(self, *args, **kwargs): 47 | super().__init__(*args, **kwargs) 48 | 49 | rho = np.tan(np.deg2rad(self.single_params.get("psi"))) * np.exp( 50 | -1j * np.deg2rad(self.single_params.get("delta")) 51 | ) 52 | theta = self.single_params.get("angle") * np.pi / 180 53 | eps = np.sin(theta) ** 2 * ( 54 | 1 + np.tan(theta) ** 2 * ((1 - rho) / (1 + rho)) ** 2 55 | ) 56 | 57 | self.interpolation = interp1d( 58 | self.single_params.get("lbda"), 59 | eps, 60 | kind="cubic", 61 | ) 62 | 63 | def __add__(self, _: Union[int, float, "Dispersion"]) -> "DispersionSum": 64 | raise NotImplementedError("Adding of tabular dispersions is not yet supported") 65 | 66 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 67 | return self.interpolation(lbda) 68 | -------------------------------------------------------------------------------- /src/elli/dispersions/sellmeier.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Sellmeier dispersion.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from .base_dispersion import Dispersion 7 | 8 | 9 | class Sellmeier(Dispersion): 10 | r"""Sellmeier dispersion. 11 | 12 | Single parameters: 13 | -- 14 | 15 | Repeated parameters: 16 | :A: Coefficient for n\ :sup:`2` contribution. Defaults to 0. 17 | :B: Resonance wavelength. Defaults to 0. Unit in µm\ :sup:`2`. 18 | 19 | Output: 20 | .. math:: 21 | \varepsilon(\lambda) = 1 + \sum_j \boldsymbol{A}_j 22 | \cdot \lambda^2 /(\lambda^2 - \boldsymbol{B}_j) 23 | 24 | With :math:`j` as the index of the respective oscillator. 25 | """ 26 | 27 | single_params_template = {} 28 | rep_params_template = {"A": 0, "B": 0} 29 | 30 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 31 | lbda = lbda / 1e3 32 | return 1 + sum( 33 | c.get("A") * lbda**2 / (lbda**2 - c.get("B")) for c in self.rep_params 34 | ) 35 | -------------------------------------------------------------------------------- /src/elli/dispersions/sellmeier_custom.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Sellmeier dispersion.""" 3 | 4 | import numpy.typing as npt 5 | 6 | from .base_dispersion import Dispersion 7 | 8 | 9 | class SellmeierCustomExponent(Dispersion): 10 | r"""Sellmeier dispersion with custom exponents. 11 | 12 | Single parameters: 13 | -- 14 | 15 | Repeated parameters: 16 | :A: Coefficient for n\ :sup:`2` contribution. Defaults to 0. 17 | :e_A: Exponent for the wavelength in the numerator. Defaults to 1. 18 | :B: Resonance wavelength. Defaults to 0. Unit in µm\ :sup:`-2`. 19 | :e_B: Exponent for B. Defaults to 1. 20 | 21 | Output: 22 | .. math:: 23 | \varepsilon(\lambda) = \sum_j \boldsymbol{A}_j 24 | \cdot \lambda^{\boldsymbol{e_A}j} /(\lambda^2 - \boldsymbol{B}_j^{\boldsymbol{e_B}j}) 25 | 26 | With :math:`j` as the index of the respective oscillator. 27 | """ 28 | 29 | single_params_template = {} 30 | rep_params_template = {"A": 0, "e_A": 1, "B": 0, "e_B": 1} 31 | 32 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 33 | lbda = lbda / 1e3 34 | return sum( 35 | c.get("A") * lbda ** c.get("e_A") / (lbda**2 - c.get("B") ** c.get("e_B")) 36 | for c in self.rep_params 37 | ) 38 | -------------------------------------------------------------------------------- /src/elli/dispersions/table_epsilon.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Dispersion specified by a table of wavelengths (nm) and dielectric function values.""" 3 | 4 | from typing import Any, Dict, Union 5 | import numpy.typing as npt 6 | import scipy.interpolate 7 | 8 | from .epsilon_inf import EpsilonInf 9 | from .base_dispersion import Dispersion, InvalidParameters, DispersionSum 10 | 11 | 12 | class TableEpsilon(Dispersion): 13 | r"""Dispersion specified by a table of wavelengths (nm) and dielectric function values. 14 | Please not that this model will produce errors for wavelengths outside the provided 15 | wavelength range. 16 | 17 | Single parameters: 18 | :lbda (list): Wavelengths in nm. This value must be provided. 19 | :epsilon: Complex dielectric function values in the convention ε1 + iε2. 20 | This value must be provided. 21 | :kind: Type of interpolation 22 | (see scipy.interpolate.interp1d for more information). Defaults to 'linear'. 23 | 24 | Repeated parameters: 25 | -- 26 | 27 | Output: 28 | The interpolation in the given wavelength range. 29 | """ 30 | 31 | single_params_template = {"lbda": None, "epsilon": None} 32 | rep_params_template: Dict[str, Any] = {} 33 | 34 | def __init__(self, *args, **kwargs) -> None: 35 | self.kind = kwargs.pop("kind", "linear") 36 | 37 | super().__init__(*args, **kwargs) 38 | 39 | if len(self.single_params.get("lbda")) == 0: 40 | raise InvalidParameters("Wavelength array cannot be of length zero.") 41 | 42 | if len(self.single_params.get("epsilon")) != len( 43 | self.single_params.get("lbda") 44 | ): 45 | raise InvalidParameters( 46 | "Wavelength and epsilon arrays must have the same length." 47 | ) 48 | 49 | self.interpolation = scipy.interpolate.interp1d( 50 | self.single_params.get("lbda"), 51 | self.single_params.get("epsilon"), 52 | kind=self.kind, 53 | ) 54 | 55 | self.default_lbda_range = self.single_params.get("lbda") 56 | 57 | def __add__(self, other: Union[int, float, "Dispersion"]) -> "DispersionSum": 58 | if isinstance(other, (int, float)): 59 | return DispersionSum(self, EpsilonInf(eps=other)) 60 | 61 | if isinstance(other, TableEpsilon): 62 | raise NotImplementedError( 63 | "Adding of tabular dispersions is not yet supported" 64 | ) 65 | 66 | if isinstance(other, Dispersion): 67 | return DispersionSum(self, other) 68 | 69 | if isinstance(other, DispersionSum): 70 | other.dispersions.append(self) 71 | return other 72 | 73 | raise TypeError( 74 | f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'" 75 | ) 76 | 77 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 78 | return self.interpolation(lbda) 79 | -------------------------------------------------------------------------------- /src/elli/dispersions/table_index.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Dispersion specified by a table of wavelengths (nm) and refractive index values.""" 3 | 4 | from typing import Union 5 | import numpy.typing as npt 6 | import scipy.interpolate 7 | 8 | from elli.dispersions.constant_refractive_index import ConstantRefractiveIndex 9 | 10 | from .base_dispersion import IndexDispersion, IndexDispersionSum, InvalidParameters 11 | 12 | 13 | class Table(IndexDispersion): 14 | """Dispersion specified by a table of wavelengths (nm) and refractive index values. 15 | Please not that this model will produce errors for wavelengths outside the provided 16 | wavelength range. 17 | 18 | Single parameters: 19 | :lbda (list): Wavelengths in nm. This value must be provided. 20 | :n: Complex refractive index values in the convention n + ik. 21 | This value must be provided. 22 | :kind: Type of interpolation 23 | (see scipy.interpolate.interp1d for more information). Defaults to 'linear'. 24 | 25 | Repeated parameters: 26 | -- 27 | 28 | Output: 29 | The interpolation in the given wavelength range. 30 | """ 31 | 32 | single_params_template = {"lbda": None, "n": None} 33 | rep_params_template = {} 34 | 35 | def __init__(self, *args, **kwargs) -> None: 36 | self.kind = kwargs.pop("kind", "linear") 37 | 38 | super().__init__(*args, **kwargs) 39 | 40 | if len(self.single_params.get("lbda")) == 0: 41 | raise InvalidParameters("Wavelength array cannot be of length zero.") 42 | 43 | if len(self.single_params.get("n")) != len(self.single_params.get("lbda")): 44 | raise InvalidParameters( 45 | "Wavelength and refractive index arrays must have the same length." 46 | ) 47 | 48 | self.interpolation = scipy.interpolate.interp1d( 49 | self.single_params.get("lbda"), 50 | self.single_params.get("n"), 51 | kind=self.kind, 52 | ) 53 | 54 | self.default_lbda_range = self.single_params.get("lbda") 55 | 56 | def __add__( 57 | self, other: Union[int, float, "IndexDispersion"] 58 | ) -> "IndexDispersionSum": 59 | if isinstance(other, (int, float)): 60 | return IndexDispersionSum(self, ConstantRefractiveIndex(eps=other)) 61 | 62 | if isinstance(other, Table): 63 | raise NotImplementedError( 64 | "Adding of tabular dispersions is not yet supported" 65 | ) 66 | 67 | if isinstance(other, IndexDispersion): 68 | return IndexDispersionSum(self, other) 69 | 70 | if isinstance(other, IndexDispersionSum): 71 | other.dispersions.append(self) 72 | return other 73 | 74 | raise TypeError( 75 | f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'" 76 | ) 77 | 78 | def refractive_index(self, lbda: npt.ArrayLike) -> npt.NDArray: 79 | return self.interpolation(lbda) 80 | -------------------------------------------------------------------------------- /src/elli/dispersions/table_spectraray.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Helper class to load spectraray's tabulated dielectric functions.""" 3 | 4 | import pandas as pd 5 | 6 | from ..utils import conversion_wavelength_energy 7 | from .table_epsilon import TableEpsilon 8 | 9 | 10 | class TableSpectraRay: 11 | """Helper class to load spectraray's tabulated dielectric functions.""" 12 | 13 | def __init__(self, path: str) -> None: 14 | """ 15 | Args: 16 | path (str): Defines the folder where the Spectraray files are saved. 17 | """ 18 | self.spectraray_path = path 19 | 20 | def load_dispersion_table(self, fname: str) -> TableEpsilon: 21 | """Load a dispersion table from a ascii file 22 | in the spectraray materials ascii format. 23 | This only accounts for tabulated dielectric function data. 24 | Spectraray also stores dispersion data in other formats, 25 | but this function is not able to read these other formats. 26 | 27 | Args: 28 | fname (str): The filename of the spectraray ascii file. 29 | 30 | Returns: 31 | TableEpsilon: A dispersion object containing the tabulated data. 32 | """ 33 | start = 0 34 | stop = 0 35 | with open(self.spectraray_path + fname, "r", encoding="utf8") as file: 36 | line = file.readline() 37 | cnt = 0 38 | while line: 39 | if line.strip() == "Begin of array": 40 | start = cnt + 1 41 | if line.strip() == "End of array": 42 | stop = cnt 43 | line = file.readline() 44 | cnt += 1 45 | 46 | if line.startswith("Units="): 47 | x_unit = line.split("=")[1].split(",")[0] 48 | 49 | df = pd.read_csv( 50 | self.spectraray_path + fname, 51 | sep=r"\s+", 52 | skiprows=start, 53 | nrows=stop - start, 54 | index_col=0, 55 | usecols=[0, 1, 2], 56 | names=[x_unit, "ϵ1", "ϵ2"], 57 | ) 58 | 59 | if x_unit == "Wavelength": 60 | return TableEpsilon( 61 | lbda=df.index, epsilon=df.loc[:, "ϵ1"] + 1j * df.iloc[:, "ϵ2"] 62 | ) 63 | return TableEpsilon( 64 | lbda=conversion_wavelength_energy(df.index), 65 | epsilon=df.loc[:, "ϵ1"] + 1j * df.loc[:, "ϵ2"], 66 | ) 67 | -------------------------------------------------------------------------------- /src/elli/dispersions/tanguy.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Fractional dimensional Tanguy model.""" 3 | 4 | import numpy as np 5 | import numpy.typing as npt 6 | from numpy.lib.scimath import sqrt 7 | 8 | # pylint: disable=no-name-in-module 9 | from scipy.special import digamma, gamma 10 | 11 | from ..utils import conversion_wavelength_energy 12 | from .base_dispersion import Dispersion 13 | 14 | 15 | class Tanguy(Dispersion): 16 | r"""Fractional dimensional Tanguy model. 17 | This model is an analytical expression of Wannier excitons, including 18 | bound and unbound states. 19 | 20 | Single parameters: 21 | :A: Amplitude (eV). Defaults to 1. 22 | :d: Dimensionality 1 < d <= 3. Defaults to 3. 23 | :gamma: Excitonic broadening (eV). Defaults to 0.1. 24 | :R: Excitonic binding energy (eV). Defaults to 0.1. 25 | :Eg: Optical band gap energy (eV). Defaults to 1. 26 | :a: Sellmeier coefficient for background dielectric constant (eV²). 27 | Defaults to 0. 28 | :b: Sellmeier coefficient for background dielectric constant (eV²). 29 | Defaults to 0. 30 | 31 | Repeated parameters. 32 | -- 33 | 34 | Output: 35 | The Tanguy dispersion. Since the formula is rather long it is not written here. 36 | Please refer to the references for a full formula. 37 | 38 | References: 39 | * C. Tanguy, Phys. Rev. Lett. 75, 4090 (1995). Errata, Phys. Rev. Lett. 76, 716 (1996). 40 | * C. Tanguy, Phys. Rev. B. 60. 10660 (1990). 41 | """ 42 | 43 | single_params_template = { 44 | "A": 1, 45 | "d": 3, 46 | "gamma": 0.1, 47 | "R": 0.1, 48 | "Eg": 1, 49 | "a": 0, 50 | "b": 0, 51 | } 52 | rep_params_template = {} 53 | 54 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 55 | E = conversion_wavelength_energy(lbda) 56 | A = self.single_params.get("A") 57 | d = self.single_params.get("d") 58 | gam = self.single_params.get("gamma") 59 | R = self.single_params.get("R") 60 | Eg = self.single_params.get("Eg") 61 | a = self.single_params.get("a") 62 | b = self.single_params.get("b") 63 | 64 | return ( 65 | 1 66 | + a / (b - E**2) 67 | + A 68 | * R ** (d / 2 - 1) 69 | / (E + 1j * gam) ** 2 70 | * ( 71 | Tanguy.g(Tanguy.xsi(E + 1j * gam, R, Eg), d) 72 | + Tanguy.g(Tanguy.xsi(-E - 1j * gam, R, Eg), d) 73 | - 2 * Tanguy.g(Tanguy.xsi(E * 0, R, Eg), d) 74 | ) 75 | ) 76 | 77 | @staticmethod 78 | def xsi(z, R, Eg): 79 | return sqrt(R / (Eg - z)) 80 | 81 | @staticmethod 82 | def g(xsi, d): 83 | if d == 2: 84 | return 2 * np.log(xsi) - 2 * digamma(0.5 - xsi) 85 | if d == 3: 86 | return 2 * np.log(xsi) - 2 * digamma(1 - xsi) - 1 / xsi 87 | 88 | D = d - 1 89 | return ( 90 | 2 91 | * np.pi 92 | * gamma(D / 2 + xsi) 93 | / gamma(D / 2) ** 2 94 | / gamma(1 - D / 2 + xsi) 95 | / xsi ** (d - 2) 96 | * (1 / np.tan(np.pi * (D / 2 - xsi)) - 1 / np.tan(np.pi * D)) 97 | ) 98 | -------------------------------------------------------------------------------- /src/elli/dispersions/tauc_lorentz.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | """Tauc-Lorentz dispersion law. Model by Jellison and Modine.""" 3 | 4 | import numpy as np 5 | import numpy.typing as npt 6 | from numpy.lib.scimath import sqrt 7 | 8 | from ..utils import conversion_wavelength_energy 9 | from .base_dispersion import Dispersion 10 | 11 | 12 | class TaucLorentz(Dispersion): 13 | """Tauc-Lorentz dispersion law. Model by Jellison and Modine. 14 | 15 | Single parameters: 16 | :Eg: Bandgap energy (eV). Defaults to 1. 17 | 18 | Repeated parameters: 19 | :A: Strength of the absorption. Typically 10 < A < 200. Defaults to 20. 20 | :E: Lorentz resonance energy (eV). Always keep E > Eg!!. Defaults to 1.5. 21 | :C: Lorentz broadening (eV). Typically 0 < Ci < 10. Defaults to 1. 22 | 23 | Output: 24 | The Tauc lorentz dispersion. Please refer to the references for a full formula. 25 | 26 | References: 27 | * G.E. Jellision and F.A. Modine, Appl. Phys. Lett. 69 (3), 371-374 (1996) 28 | * Erratum, G.E. Jellison and F.A. Modine, Appl. Phys. Lett 69 (14), 2137 (1996) 29 | * H. Chen, W.Z. Shen, Eur. Phys. J. B. 43, 503-507 (2005) 30 | """ 31 | 32 | single_params_template = {"Eg": 1} 33 | rep_params_template = {"A": 20, "E": 1.5, "C": 1} 34 | 35 | @staticmethod 36 | def eps1(E, Eg, Ai, Ei, Ci): 37 | gamma2 = sqrt(Ei**2 - Ci**2 / 2) ** 2 38 | alpha = sqrt(4 * Ei**2 - Ci**2) 39 | aL = (Eg**2 - Ei**2) * E**2 + Eg**2 * Ci**2 - Ei**2 * (Ei**2 + 3 * Eg**2) 40 | aA = (E**2 - Ei**2) * (Ei**2 + Eg**2) + Eg**2 * Ci**2 41 | zeta4 = (E**2 - gamma2) ** 2 + alpha**2 * Ci**2 / 4 42 | 43 | # fmt: off 44 | return ( 45 | Ai*Ci*aL/2.0/np.pi/zeta4/alpha/Ei*np.log((Ei**2 + Eg**2 + alpha*Eg)/(Ei**2 + Eg**2 - alpha*Eg)) - \ 46 | Ai*aA/np.pi/zeta4/Ei*(np.pi - np.arctan((2.0*Eg + alpha)/Ci) + np.arctan((alpha - 2.0*Eg)/Ci)) + \ 47 | 2.0*Ai*Ei*Eg/np.pi/zeta4/alpha*(E**2 - gamma2)*(np.pi + 2.0*np.arctan(2.0/alpha/Ci*(gamma2 - Eg**2))) - \ 48 | Ai*Ei*Ci*(E**2 + Eg**2)/np.pi/zeta4/E*np.log(abs(E - Eg)/(E + Eg)) + \ 49 | 2.0*Ai*Ei*Ci*Eg/np.pi/zeta4 * \ 50 | np.log(abs(E - Eg) * (E + Eg) / sqrt((Ei**2 - Eg**2)**2 + Eg**2 * Ci**2)) 51 | ) 52 | # fmt: on 53 | 54 | def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: 55 | energy = conversion_wavelength_energy(lbda) 56 | energy_g = self.single_params.get("Eg") 57 | return sum( 58 | ( 59 | 1j 60 | * ( 61 | c.get("A") 62 | * c.get("E") 63 | * c.get("C") 64 | * (energy - energy_g) ** 2 65 | / ((energy**2 - c.get("E") ** 2) ** 2 + c.get("C") ** 2 * energy**2) 66 | / energy 67 | ) 68 | * np.heaviside(energy - energy_g, 0) 69 | + self.eps1(energy, energy_g, c.get("A"), c.get("E"), c.get("C")) 70 | ) 71 | for c in self.rep_params 72 | ) 73 | -------------------------------------------------------------------------------- /src/elli/fitting/__init__.py: -------------------------------------------------------------------------------- 1 | from .decorator_mmatrix import fit_mueller_matrix 2 | from .decorator_psi_delta import fit 3 | from .params_hist import ParamsHist 4 | -------------------------------------------------------------------------------- /src/elli/fitting/params_hist.py: -------------------------------------------------------------------------------- 1 | """ParmsHist provides a wrapper around lmfit.Parameters 2 | to keep track of the changes made to the parameters.""" 3 | 4 | # Encoding: utf-8 5 | import copy 6 | from typing import List 7 | 8 | try: 9 | from lmfit import Parameters 10 | except ImportError as e: 11 | raise ImportError( 12 | "This module requires lmfit to work properly.\n" 13 | "Try installing this package with the additional fitting requirement, " 14 | "i.e. pip install pyElli[fitting]" 15 | ) from e 16 | 17 | 18 | class ParamsHist(Parameters): 19 | """A wrapper around lmfit.Parameters to keep track of the changes made to the parameters.""" 20 | 21 | def __init__(self) -> None: 22 | super().__init__() 23 | self._history = [] 24 | self._max_length = 50 25 | 26 | @property 27 | def history(self) -> List[Parameters]: 28 | """Gets the entire history 29 | 30 | Returns: 31 | List[Parameters]: The history 32 | """ 33 | return self._history 34 | 35 | def clear_history(self) -> None: 36 | """Clears the parameters history""" 37 | self._history = [] 38 | 39 | @property 40 | def history_len(self) -> int: 41 | """The current length of the history 42 | 43 | Returns: 44 | int: The length of the history 45 | """ 46 | return len(self._history) 47 | 48 | @property 49 | def max_history_len(self) -> int: 50 | """The maximum length of the history 51 | 52 | Returns: 53 | int: The maximum length of the history 54 | """ 55 | return self._max_length 56 | 57 | @max_history_len.setter 58 | def max_history_len(self, history_len: int) -> None: 59 | """Sets the maximum history length. If the current 60 | history length is greater than the new history length the history 61 | gets truncated. 62 | 63 | Args: 64 | history_len (int): The new history length 65 | 66 | Raises: 67 | ValueError: If history_len is not an int or < 1. 68 | """ 69 | if not isinstance(history_len, int): 70 | raise ValueError("History length has to be an integer") 71 | 72 | if history_len < 1: 73 | raise ValueError("History length must be greater than 0") 74 | 75 | self._history = self._history[-history_len:] 76 | self._max_length = history_len 77 | 78 | def revert(self, hist_pos: int) -> None: 79 | """Reverts to an older history version and keeps the entire history. 80 | 81 | Args: 82 | hist_pos (int): The history position to revert to. 83 | """ 84 | if len(self._history) > (hist_pos % len(self._history)): 85 | self.update(self._history[hist_pos]) 86 | 87 | def pop(self): 88 | """Gets to the previous history version and deletes the current element.""" 89 | if len(self._history) > 0: 90 | curr_params = self.copy() 91 | self.update(self._history[-1]) 92 | self._history = self._history[:-1] 93 | 94 | return curr_params 95 | return None 96 | 97 | def update_value(self, key: str, value: float) -> None: 98 | """Updates a parameter and keeps track of the change in history 99 | 100 | Args: 101 | key (str): The key to be updated 102 | value (float): The value the key should be updated to 103 | """ 104 | self.commit() 105 | super().__getitem__(key).value = value 106 | 107 | def update_params(self, parameters) -> None: 108 | """Updates the current parameters from a lmfit parameters object. 109 | 110 | Args: 111 | parameters (lmfit.Parameters): 112 | The lmfit parameters object to update the values from. 113 | """ 114 | self.commit() 115 | self.update(parameters) 116 | 117 | def tracked_add(self, *args, **kwargs) -> None: 118 | """Adds a parameter and keeps track of the change in history""" 119 | self.commit() 120 | super().add(*args, **kwargs) 121 | 122 | def commit(self) -> None: 123 | """Saves the current parameter set to history.""" 124 | if len(self._history) >= self._max_length: 125 | self._history = self._history[1:] 126 | clone = copy.deepcopy(self) 127 | self.history.append(clone) 128 | -------------------------------------------------------------------------------- /src/elli/formula_parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/src/elli/formula_parser/__init__.py -------------------------------------------------------------------------------- /src/elli/formula_parser/dispersion_function_grammar.lark: -------------------------------------------------------------------------------- 1 | ?assignment: "eps" "=" kkr_expression -> eps 2 | | "n" "=" kkr_expression -> n 3 | 4 | ?kkr_expression: expression 5 | | "" "+" "1j" "*" term -> kkr_term 6 | 7 | ?expression: term 8 | | expression "+" term -> add 9 | | expression "-" term -> sub 10 | 11 | ?term: factor 12 | | term "*" factor -> mul 13 | | term "/" factor -> div 14 | 15 | ?factor: power 16 | | power "**" power -> power 17 | 18 | 19 | ?power: "(" expression ")" 20 | | FUNC "(" expression ")" -> func 21 | | "sum" "[" repeated_expression "]" -> sum_expr 22 | | NAME -> single_param_name 23 | | SIGNED_NUMBER -> number 24 | | BUILTIN -> builtin 25 | 26 | ?repeated_expression: repeated_term 27 | | repeated_expression "+" repeated_term -> add 28 | | repeated_expression "-" repeated_term -> sub 29 | 30 | 31 | ?repeated_term: repeated_factor 32 | | repeated_term "*" repeated_factor -> mul 33 | | repeated_term "/" repeated_factor -> div 34 | 35 | ?repeated_factor: repeated_power 36 | | repeated_power "**" repeated_power -> power 37 | 38 | ?repeated_power: "(" repeated_expression ")" 39 | | FUNC "(" repeated_expression ")" -> func 40 | | SIGNED_NUMBER -> number 41 | | NAME -> param_name 42 | | BUILTIN -> builtin 43 | 44 | FUNC.1: "sin" | "cos" | "tan" | "sqrt" | "dawsn" | "ln" | "log" | "heaviside" 45 | BUILTIN.1: "1j" | "pi" | "eps_0" | "hbar" | "h" | "c" 46 | 47 | %import common.CNAME -> NAME 48 | %import common.SIGNED_NUMBER 49 | %import common.WS_INLINE 50 | 51 | %ignore WS_INLINE -------------------------------------------------------------------------------- /src/elli/importer/__init__.py: -------------------------------------------------------------------------------- 1 | import chardet 2 | 3 | 4 | def detect_encoding(fname: str) -> str: 5 | r"""Detects the encoding of file fname. 6 | Args: 7 | fname (str): Filename 8 | Returns: 9 | str: Encoding identifier string. 10 | """ 11 | with open(fname, "rb") as f: 12 | raw_data = f.read() 13 | result = chardet.detect(raw_data) 14 | return result["encoding"] 15 | -------------------------------------------------------------------------------- /src/elli/importer/accurion.py: -------------------------------------------------------------------------------- 1 | """A helper class to load data from Accurion EP4 DAT files. 2 | Typical files look like: Si3N4_on_4inBF33_W03_20240905-105631.ds.dat 3 | """ 4 | 5 | import numpy as np 6 | import pandas as pd 7 | 8 | from ..utils import convert_delta_range 9 | from . import detect_encoding 10 | 11 | 12 | def read_accurion_psi_delta(fname: str) -> pd.DataFrame: 13 | r"""Read a psi/delta Accurion dat file. 14 | 15 | Args: 16 | fname (str): Filename of the measured dat file 17 | 18 | Returns: 19 | pd.DataFrame: DataFrame containing the psi/delta data in the pyElli-compatible format. 20 | """ 21 | encoding = detect_encoding(fname) 22 | psi_delta_df = pd.read_csv( 23 | fname, delimiter="\t", encoding=encoding, skiprows=0, header=0 24 | )[1:].astype(float) 25 | psi_delta_df = psi_delta_df.reindex(columns=list(["AOI", "Lambda", "Delta", "Psi"])) 26 | psi_delta_df = psi_delta_df.rename( 27 | columns={ 28 | "AOI": "Angle of Incidence", 29 | "Lambda": "Wavelength", 30 | "Delta": "Δ", 31 | "Psi": "Ψ", 32 | } 33 | ) 34 | psi_delta_df = psi_delta_df.groupby(["Angle of Incidence", "Wavelength"]).sum() 35 | 36 | # wrap delta range 37 | psi_delta_df.loc[:, "Δ"] = convert_delta_range(psi_delta_df.loc[:, "Δ"], -180, 180) 38 | 39 | return psi_delta_df 40 | -------------------------------------------------------------------------------- /src/elli/importer/spectraray.py: -------------------------------------------------------------------------------- 1 | """A helper class to load data from SpectraRay ASCII Files. 2 | It only supplies a rudimentary loading of standard psi/delta values 3 | and misses some other features. 4 | """ 5 | 6 | import re 7 | 8 | import pandas as pd 9 | from packaging.version import Version, parse 10 | 11 | from ..utils import calc_rho, convert_delta_range 12 | from . import detect_encoding 13 | 14 | 15 | def read_spectraray_psi_delta( 16 | fname: str, sep: str = r"\s+", decimal: str = "." 17 | ) -> pd.DataFrame: 18 | r"""Read a psi/delta spectraray ascii file. 19 | 20 | Args: 21 | fname (str): Filename of the measurement ascii file. 22 | sep (str, optional): Data separator in the datafile. Defaults to "\s+". 23 | decimal (str, optional): Decimal separator in the datafile. Defaults to ".". 24 | 25 | Returns: 26 | pd.DataFrame: DataFrame containing the psi/delta data in 27 | the format to be further processes inside pyElli. 28 | """ 29 | # detect encoding 30 | encoding = detect_encoding(fname) 31 | 32 | # read data and drop empty column 33 | psi_delta_df = pd.read_csv( 34 | fname, 35 | encoding=encoding, 36 | index_col=0, 37 | header=None, 38 | sep=sep, 39 | decimal=decimal, 40 | skiprows=1, 41 | ) 42 | psi_delta_df.dropna(axis="columns", how="all", inplace=True) 43 | 44 | # index data correctly 45 | psi_delta_df.index.name = "Wavelength" 46 | 47 | with open(fname) as f: 48 | header = f.readlines()[0] 49 | 50 | aois = list(map(float, re.split(sep, header)[3::2])) 51 | index = pd.MultiIndex.from_product( 52 | [aois, ["Ψ", "Δ"]], names=["Angle of Incidence", ""] 53 | ) 54 | psi_delta_df.columns = index 55 | 56 | # reorder dataframe 57 | if Version("2.2") <= parse(pd.__version__) < Version("3.0"): 58 | psi_delta_df = psi_delta_df.stack(0, future_stack=True) 59 | else: 60 | psi_delta_df = psi_delta_df.stack(0) 61 | psi_delta_df = psi_delta_df.reorder_levels(["Angle of Incidence", "Wavelength"]) 62 | psi_delta_df.sort_index(axis=0, inplace=True) 63 | psi_delta_df.sort_index(axis=1, ascending=False, inplace=True) 64 | 65 | # convert delta range 66 | psi_delta_df.loc[:, "Δ"] = convert_delta_range(psi_delta_df.loc[:, "Δ"], -180, 180) 67 | 68 | return psi_delta_df 69 | 70 | 71 | def read_spectraray_mmatrix( 72 | fname: str, sep: str = r"\s+", decimal: str = "." 73 | ) -> pd.DataFrame: 74 | r"""Read a mueller matrix spectraray ascii file. 75 | Only reads the first entry and does not support reading multiple angles. 76 | For multiple angles you have to save the data in multiple files. 77 | 78 | Args: 79 | fname (str): Filename of the measurement ascii file. 80 | sep (str, optional): Data separator in the datafile. Defaults to "\s+". 81 | decimal (str, optional): Decimal separator in the datafile. Defaults to ".". 82 | 83 | Returns: 84 | pd.DataFrame: DataFrame containing the psi/delta data in 85 | the format to be further processes inside pyElli. 86 | """ 87 | encoding = detect_encoding(fname) 88 | 89 | mueller_matrix = pd.read_csv( 90 | fname, encoding=encoding, sep=sep, decimal=decimal, index_col=0 91 | ).iloc[:, -17:-1] 92 | mueller_matrix.index.name = "Wavelength" 93 | mueller_matrix.columns = [ 94 | "M11", 95 | "M12", 96 | "M13", 97 | "M14", 98 | "M21", 99 | "M22", 100 | "M23", 101 | "M24", 102 | "M31", 103 | "M32", 104 | "M33", 105 | "M34", 106 | "M41", 107 | "M42", 108 | "M43", 109 | "M44", 110 | ] 111 | 112 | return mueller_matrix 113 | 114 | 115 | def read_spectraray_rho( 116 | fname: str, sep: str = r"\s+", decimal: str = "." 117 | ) -> pd.DataFrame: 118 | r"""Read a psi/delta spectraray ascii file and converts it to rho values. 119 | 120 | Args: 121 | fname (str): Filename of the measurement ascii file. 122 | sep (str, optional): Data separator in the datafile. Defaults to "\s+". 123 | decimal (str, optional): Decimal separator in the datafile. Defaults to ".". 124 | 125 | Returns: 126 | pd.DataFrame: DataFrame containing the rho data in 127 | the format to be further processes inside pyElli. 128 | """ 129 | psi_delta = read_spectraray_psi_delta(fname, sep, decimal) 130 | return calc_rho(psi_delta) 131 | -------------------------------------------------------------------------------- /src/elli/kkr/__init__.py: -------------------------------------------------------------------------------- 1 | from .kkr import * 2 | -------------------------------------------------------------------------------- /src/elli/plot/__init__.py: -------------------------------------------------------------------------------- 1 | from .mueller_matrix import plot_mmatrix 2 | from .structure import draw_structure 3 | -------------------------------------------------------------------------------- /src/elli/plot/mueller_matrix.py: -------------------------------------------------------------------------------- 1 | """Plotting functions for Mueller matrices""" 2 | 3 | # Encoding: utf-8 4 | from typing import List, Union, Optional 5 | 6 | import pandas as pd 7 | 8 | try: 9 | import plotly.graph_objects as go 10 | from plotly.subplots import make_subplots 11 | except ImportError as e: 12 | raise ImportError( 13 | "Optional dependency plotly is not installed. This module will not work properly.\n" 14 | "Try installing this package with the additional fitting requirement, " 15 | "i.e. pip install pyElli[fitting]" 16 | ) from e 17 | 18 | COLORS = [ 19 | "#636EFA", 20 | "#EF553B", 21 | "#00CC96", 22 | "#AB63FA", 23 | "#FFA15A", 24 | "#19D3F3", 25 | "#FF6692", 26 | "#B6E880", 27 | "#FF97FF", 28 | "#FECB52", 29 | ] 30 | 31 | 32 | def plot_mmatrix( 33 | dataframes: Union[pd.DataFrame, List[pd.DataFrame]], 34 | colors: Optional[List[str]] = None, 35 | dashes: Optional[List[str]] = None, 36 | names: Optional[List[str]] = None, 37 | single: bool = True, 38 | full_scale: bool = False, 39 | sharex: bool = False, 40 | ) -> go.Figure: 41 | """Takes multiple Mueller matrix dataframes with columns Mxy for matrix postion x,y 42 | and plots them together. Needs plotly as additional requirement to work. 43 | 44 | Args: 45 | dataframes (Union[pd.Dataframe, List[pd.DataFrame]]): 46 | A dataframe or a list of dataframes containing data of the same index. 47 | colors (Optional[List[str]], optional): 48 | A list of colors which are cycled for each dataframes index. Defaults to None. 49 | dashes (Optional[List[str]], optional): 50 | A list of dash line styles which are cycled for each dataframes index. Defaults to None. 51 | names (Optional[List[str]], optional): A name for each dataframe index. Defaults to None. 52 | single (bool, optional): Uses a single plot if set and a grid if not set. Defaults to True. 53 | full_scale (bool, optional): Sets the y-axis limits to [-1, 1] if set. Defaults to False. 54 | sharex (bool, optional): 55 | Ties the zooming of the x-axis together for each plot in grid view. Defaults to False. 56 | 57 | Returns: 58 | go.Figure: A plotly figure containing the data from dataframes as a grid or single view. 59 | """ 60 | if isinstance(dataframes, pd.DataFrame): 61 | dataframes = [dataframes] 62 | 63 | if colors is None: 64 | colors = COLORS 65 | if dashes is None: 66 | dashes = ["solid", "dash", "dot"] 67 | if names is None: 68 | names = ["", "theory"] 69 | 70 | if single: 71 | fig = go.Figure() 72 | fig.update_layout( 73 | yaxis_title="Müller Matrix Elements", xaxis_title="Wavelength (nm)" 74 | ) 75 | else: 76 | fig = make_subplots(rows=4, cols=4) 77 | if full_scale: 78 | fig.update_yaxes(range=[-1, 1]) 79 | 80 | for i, melem in enumerate(dataframes[0]): 81 | coli = colors[i % len(colors)] 82 | for j, mueller_df in enumerate(dataframes): 83 | dashi = dashes[j % len(dashes)] 84 | namesi = names[j % len(names)] 85 | if single: 86 | fig.add_trace( 87 | go.Scatter( 88 | x=mueller_df.index, 89 | y=mueller_df[melem], 90 | name=f"{melem} {namesi}", 91 | line=dict(color=coli, dash=dashi), 92 | ) 93 | ) 94 | else: 95 | fig.add_trace( 96 | go.Scatter( 97 | x=mueller_df.index, 98 | y=mueller_df[melem], 99 | name=f"{melem} {namesi}", 100 | line=dict(color=coli, dash=dashi), 101 | ), 102 | row=1 if single else i // 4 + 1, 103 | col=1 if single else i % 4 + 1, 104 | ) 105 | if sharex: 106 | fig.update_xaxes(matches="x") 107 | return go.FigureWidget(fig) 108 | -------------------------------------------------------------------------------- /src/elli/plot/structure.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | import numpy as np 3 | from numpy.lib.scimath import sqrt 4 | 5 | try: 6 | import matplotlib.pyplot as plt 7 | except ImportError as e: 8 | raise ImportError( 9 | "Optional dependency matplotlib missing. This module will not work properly.\n" 10 | "Try installing this package with the additional fitting requirement, " 11 | "i.e. pip install pyElli[fitting]" 12 | ) from e 13 | 14 | from ..utils import E_X 15 | 16 | 17 | def get_permittivity_profile(structure, lbda): 18 | """Returns permittivity tensor profile.""" 19 | layers = [] 20 | for L in structure.layers: 21 | layers.extend(L.get_permittivity_profile(lbda)) 22 | front = (float("inf"), structure.front_material.get_tensor(lbda)) 23 | back = (float("inf"), structure.back_material.get_tensor(lbda)) 24 | return sum([[front], layers, [back]], []) 25 | 26 | 27 | def get_index_profile(structure, lbda, v=E_X): 28 | """Returns refractive index profile. 29 | 30 | 'v' : Unit vector, direction of evaluation of the refraction index. 31 | Default value is v = e_x. 32 | """ 33 | profile = get_permittivity_profile(structure, lbda) 34 | (h, epsilon) = list(zip(*profile)) # unzip 35 | n = [sqrt((v.T * eps * v)[0, 0, 0]) for eps in epsilon] 36 | return list(zip(h, n)) 37 | 38 | 39 | def draw_structure(structure, lbda=1000, method="graph", margin=0.15): 40 | """Draw the structure. 41 | 42 | 'method' : 'graph' or 'section' 43 | Returns : Axes object 44 | """ 45 | # Build index profile 46 | profile = get_index_profile(structure, lbda) 47 | (h, n) = list(zip(*profile)) # unzip 48 | n = np.array(n) 49 | z_layers = np.hstack((0.0, np.cumsum(h[1:-1]))) 50 | z_max = z_layers[-1] 51 | if z_max != 0.0: 52 | z_margin = margin * z_max 53 | else: 54 | z_margin = 1e-6 55 | z = np.hstack((-z_margin, z_layers, z_max + z_margin)) 56 | # Call specialized methods 57 | if method == "graph": 58 | ax = _draw_structure_graph(structure, z, n) 59 | elif method == "section": 60 | ax = _draw_structure_section(structure, z, n) 61 | else: 62 | ax = None 63 | return ax 64 | 65 | 66 | def _draw_structure_graph(structure, z, n): 67 | """Draw a graph of the refractive index profile""" 68 | n = np.hstack((n, n[-1])) 69 | # Draw the graph 70 | fig = plt.figure(figsize=(8, 3)) 71 | ax = fig.add_subplot(1, 1, 1) 72 | fig.subplots_adjust(bottom=0.17) 73 | ax.step(z, n.real, "black", where="post") 74 | ax.spines["top"].set_visible(False) 75 | ax.xaxis.set_ticks_position("bottom") 76 | ax.set_xlabel("z (nm)") 77 | ax.set_ylabel("n'") 78 | ax.set_xlim(z.min(), z.max()) 79 | ax.set_ylim(bottom=1.0) 80 | return ax 81 | 82 | 83 | def _draw_structure_section(structure, z, n): 84 | """Draw a cross section of the structure""" 85 | # Prepare arrays for pcolormesh() 86 | X = z * np.ones((2, 1)) 87 | Y = np.array([0, 1]).reshape((2, 1)) * np.ones_like(z) 88 | n = np.array(n).reshape((1, -1)).real 89 | # Draw the cross section 90 | fig = plt.figure(figsize=(8, 3)) 91 | ax = fig.add_subplot(1, 1, 1) 92 | fig.subplots_adjust(left=0.05, bottom=0.15) 93 | ax.set_yticks([]) 94 | ax.set_xlabel("z (nm)") 95 | ax.set_xlim(z.min(), z.max()) 96 | stack = ax.pcolormesh(X, Y, n, cmap=plt.get_cmap("gray_r")) 97 | colbar = fig.colorbar( 98 | stack, orientation="vertical", anchor=(1.2, 0.5), fraction=0.05 99 | ) 100 | colbar.ax.set_xlabel("n'", position=(3, 0)) 101 | return ax 102 | -------------------------------------------------------------------------------- /src/elli/solver.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | from abc import ABC, abstractmethod 3 | from copy import deepcopy 4 | 5 | from .result import Result 6 | 7 | 8 | class Solver(ABC): 9 | """ 10 | Solver base class to evaluate Experiment objects. 11 | Here the experiment and structure get unpacked 12 | and the simulation results get returned. 13 | 14 | The actual simulation is handled by subclasses. 15 | Therefore, this class should never be called directly. 16 | """ 17 | 18 | experiment = None 19 | structure = None 20 | lbda = None 21 | theta_i = None 22 | jones_vector = None 23 | permittivity_profile = None 24 | 25 | @abstractmethod 26 | def calculate(self) -> Result: 27 | pass 28 | 29 | def __init__(self, experiment: "Experiment") -> None: 30 | self.experiment = deepcopy(experiment) 31 | self.structure = self.experiment.structure 32 | self.lbda = self.experiment.lbda 33 | self.theta_i = self.experiment.theta_i 34 | self.jones_vector = self.experiment.jones_vector 35 | self.permittivity_profile = self.structure.get_permittivity_profile(self.lbda) 36 | -------------------------------------------------------------------------------- /src/elli/solver2x2.py: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | import warnings 3 | 4 | import numpy as np 5 | from numpy.lib.scimath import arcsin, sqrt 6 | 7 | from .result import Result 8 | from .solver import Solver 9 | 10 | 11 | class Solver2x2(Solver): 12 | """ 13 | Solver class to evaluate Experiment objects. 14 | Simple but fast 2x2 transfer matrix method. 15 | Cannot handle anisotropy or anything fancy, 16 | thus Jonas and Mueller matrices cannot be calculated (respective functions return None). 17 | """ 18 | 19 | def list_snell(self, n_list): 20 | angles = arcsin(n_list[0] * np.sin(np.deg2rad(self.theta_i)) / n_list) 21 | 22 | angles[0] = np.where( 23 | np.invert(Solver2x2.is_forward_angle(n_list[0], angles[0])), 24 | np.pi - angles[0], 25 | angles[0], 26 | ) 27 | angles[-1] = np.where( 28 | np.invert(Solver2x2.is_forward_angle(n_list[-1], angles[-1])), 29 | np.pi - angles[-1], 30 | angles[-1], 31 | ) 32 | 33 | return angles 34 | 35 | def calculate(self) -> Result: 36 | """Calculates the transfer matrix for the given material stack""" 37 | if len(self.permittivity_profile) > 2: 38 | d, eps = list(zip(*self.permittivity_profile[1:-1])) 39 | d_list = np.array(d) 40 | n_list = sqrt( 41 | np.vstack( 42 | [ 43 | self.permittivity_profile[0][1][:, 0, 0], 44 | np.array(eps)[..., 0, 0], 45 | self.permittivity_profile[-1][1][:, 0, 0], 46 | ] 47 | ) 48 | ) 49 | else: 50 | d_list = np.array([]) 51 | n_list = sqrt( 52 | np.vstack( 53 | [ 54 | self.permittivity_profile[0][1][:, 0, 0], 55 | self.permittivity_profile[-1][1][:, 0, 0], 56 | ] 57 | ) 58 | ) 59 | 60 | for layer in n_list: 61 | if np.any(np.logical_and(layer.real > 0, layer.imag < 0)): 62 | warnings.warn( 63 | """Solver2x2 can't handle active media (n > 0 and k < 0). 64 | Check if all materials are defined correctly or switch to Solver4x4 instead.""" 65 | ) 66 | elif np.any(np.logical_and(layer.real < 0, layer.imag > 0)): 67 | warnings.warn( 68 | """Solver2x2 can't handle media with n < 0 and k > 0. 69 | Check if all materials are defined correctly or switch to Solver4x4 instead.""" 70 | ) 71 | 72 | num_layers = n_list.shape[0] 73 | th_list = self.list_snell(n_list) 74 | kz_list = 2 * np.pi * n_list * np.cos(th_list) / self.lbda 75 | 76 | delta = kz_list[1:-1] * (d_list if n_list.ndim == 1 else d_list[:, None]) 77 | 78 | esum = "ij...,jk...->ik..." 79 | ones = np.repeat(1, n_list.shape[1]) if n_list.ndim > 1 else 1 80 | 81 | rs, rp, ts, tp = Solver2x2.fresnel(n_list[0], n_list[1], th_list[0], th_list[1]) 82 | Ms = np.array([[ones, rs], [rs, ones]], dtype=complex) / ts 83 | Mp = np.array([[ones, rp], [rp, ones]], dtype=complex) / tp 84 | 85 | for i in range(1, num_layers - 1): 86 | rs, rp, ts, tp = Solver2x2.fresnel( 87 | n_list[i], n_list[i + 1], th_list[i], th_list[i + 1] 88 | ) 89 | em = np.exp(-1j * delta[i - 1]) 90 | ep = np.exp(1j * delta[i - 1]) 91 | 92 | Ms = np.einsum( 93 | esum, Ms, np.array([[em, rs * em], [rs * ep, ep]], dtype=complex) / ts 94 | ) 95 | Mp = np.einsum( 96 | esum, Mp, np.array([[em, rp * em], [rp * ep, ep]], dtype=complex) / tp 97 | ) 98 | 99 | rtots = Ms[1, 0] / Ms[0, 0] 100 | ttots = 1 / Ms[0, 0] 101 | rtotp = Mp[1, 0] / Mp[0, 0] 102 | ttotp = 1 / Mp[0, 0] 103 | 104 | zeros = np.repeat(0, n_list.shape[1]) if n_list.ndim > 1 else 1 105 | 106 | jones_matrix_r = np.moveaxis(np.array([[rtotp, zeros], [zeros, rtots]]), 2, 0) 107 | jones_matrix_t = np.moveaxis(np.array([[ttotp, zeros], [zeros, ttots]]), 2, 0) 108 | 109 | # TODO: Test if p and s correction formulas are needed. 110 | power_correction = ((n_list[-1] * np.cos(th_list[-1])).real) / ( 111 | n_list[0] * np.cos(th_list[0]) 112 | ).real 113 | 114 | return Result(self.experiment, jones_matrix_r, jones_matrix_t, power_correction) 115 | 116 | @staticmethod 117 | def fresnel(n_i, n_t, th_i, th_t): 118 | r"""Calculate fresnel coefficients at the interface of two materials 119 | 120 | Args: 121 | n_i: Refractive index of the material of the incident wave 122 | n_t: Refractive index of the material of the transmitted wave 123 | thi_i: Incident angle of the incident wave 124 | thi_t: Refracted angle of the transmitted wave 125 | 126 | Returns: 127 | r_s: s-polarized reflection coefficient 128 | r_p: p-polarized reflection coefficient 129 | t_s: s-polarized transmission coefficient 130 | t_p: p-polarized transmission coefficient 131 | """ 132 | cos_i = np.cos(th_i) 133 | cos_t = np.cos(th_t) 134 | 135 | r_s = (n_i * cos_i - n_t * cos_t) / (n_i * cos_i + n_t * cos_t) 136 | r_p = (n_t * cos_i - n_i * cos_t) / (n_t * cos_i + n_i * cos_t) 137 | t_s = 2 * n_i * cos_i / (n_i * cos_i + n_t * cos_t) 138 | t_p = 2 * n_i * cos_i / (n_t * cos_i + n_i * cos_t) 139 | 140 | return r_s, r_p, t_s, t_p 141 | 142 | @staticmethod 143 | def is_forward_angle(n, theta): 144 | ncostheta = n * np.cos(theta) 145 | return np.where( 146 | abs(ncostheta.imag) > 1e-10, ncostheta.imag > 0, ncostheta.real > 0 147 | ) 148 | -------------------------------------------------------------------------------- /src/elli/units.py: -------------------------------------------------------------------------------- 1 | """The pint unit registry for pyElli""" 2 | 3 | from pint import UnitRegistry 4 | 5 | ureg = UnitRegistry() 6 | ureg.enable_contexts("spectroscopy") 7 | 8 | ureg.define("Angstroms = angstrom") 9 | -------------------------------------------------------------------------------- /tests/benchmark_formula_dispersion.py: -------------------------------------------------------------------------------- 1 | """Benchmark for using the formula dispersion""" 2 | 3 | from pytest import fixture 4 | import numpy as np 5 | 6 | import elli 7 | from elli.fitting import ParamsHist 8 | 9 | 10 | wavelength = np.linspace(400, 800, 500) 11 | PHI = 70 12 | 13 | 14 | @fixture 15 | def structure(): 16 | """Build a structure with a formula dispersion""" 17 | params = ParamsHist() 18 | params.add("SiO2_n0", value=1.452, min=-100, max=100, vary=False) 19 | params.add("SiO2_n1", value=36.0, min=-40000, max=40000, vary=False) 20 | params.add("SiO2_n2", value=0, min=-40000, max=40000, vary=False) 21 | params.add("SiO2_k0", value=0, min=-100, max=100, vary=False) 22 | params.add("SiO2_k1", value=0, min=-40000, max=40000, vary=False) 23 | params.add("SiO2_k2", value=0, min=-40000, max=40000, vary=False) 24 | params.add("SiO2_d", value=276.36, min=0, max=40000, vary=False) 25 | 26 | params.add("TiO2_n0", value=2.236, min=-100, max=100, vary=True) 27 | params.add("TiO2_n1", value=451, min=-40000, max=40000, vary=True) 28 | params.add("TiO2_n2", value=251, min=-40000, max=40000, vary=True) 29 | params.add("TiO2_k0", value=0, min=-100, max=100, vary=False) 30 | params.add("TiO2_k1", value=0, min=-40000, max=40000, vary=False) 31 | params.add("TiO2_k2", value=0, min=-40000, max=40000, vary=False) 32 | 33 | params.add("TiO2_d", value=20, min=0, max=40000, vary=True) 34 | 35 | SiO2 = elli.FormulaIndex( 36 | "n = n0 + 1e2 * n1 / lbda ** 2 + 1e7 * n2 / lbda ** 4 + " 37 | "1j * (k0 + 1e2 * k1 / lbda ** 2 + 1e7 * k2 / lbda ** 4)", 38 | "lbda", 39 | { 40 | "n0": params["SiO2_n0"].value, 41 | "n1": params["SiO2_n1"].value, 42 | "n2": params["SiO2_n2"].value, 43 | "k0": params["SiO2_k0"].value, 44 | "k1": params["SiO2_k1"].value, 45 | "k2": params["SiO2_k2"].value, 46 | }, 47 | {}, 48 | "nm", 49 | ).get_mat() 50 | TiO2 = elli.FormulaIndex( 51 | "n = n0 + 1e2 * n1 / lbda ** 2 + 1e7 * n2 / lbda ** 4 + " 52 | "1j * (k0 + 1e2 * k1 / lbda ** 2 + 1e7 * k2 / lbda ** 4)", 53 | "lbda", 54 | { 55 | "n0": params["TiO2_n0"].value, 56 | "n1": params["TiO2_n1"].value, 57 | "n2": params["TiO2_n2"].value, 58 | "k0": params["TiO2_k0"].value, 59 | "k1": params["TiO2_k1"].value, 60 | "k2": params["TiO2_k2"].value, 61 | }, 62 | {}, 63 | "nm", 64 | ).get_mat() 65 | 66 | Layer = [ 67 | elli.Layer(TiO2, params["TiO2_d"]), 68 | elli.Layer(SiO2, params["SiO2_d"]), 69 | elli.Layer(TiO2, params["TiO2_d"]), 70 | elli.Layer(SiO2, params["SiO2_d"]), 71 | elli.Layer(TiO2, params["TiO2_d"]), 72 | elli.Layer(SiO2, params["SiO2_d"]), 73 | elli.Layer(TiO2, params["TiO2_d"]), 74 | elli.Layer(SiO2, params["SiO2_d"]), 75 | ] 76 | 77 | return elli.Structure(elli.AIR, Layer, elli.AIR) 78 | 79 | 80 | def test_formula_solver2x2(benchmark, structure): 81 | """Benchmarks solver2x2""" 82 | benchmark.pedantic( 83 | structure.evaluate, 84 | args=(wavelength, PHI), 85 | kwargs={"solver": elli.Solver2x2}, 86 | iterations=1, 87 | rounds=10, 88 | ) 89 | 90 | 91 | def test_formula_solver4x4_expm(benchmark, structure): 92 | """Benchmarks solver2x2""" 93 | benchmark.pedantic( 94 | structure.evaluate, 95 | args=(wavelength, PHI), 96 | kwargs={"solver": elli.Solver4x4, "propagator": elli.PropagatorExpm()}, 97 | iterations=1, 98 | rounds=10, 99 | ) 100 | -------------------------------------------------------------------------------- /tests/benchmark_propagators_TiO2.py: -------------------------------------------------------------------------------- 1 | """Testing benchmark for each solver""" 2 | 3 | import elli 4 | import numpy as np 5 | from elli.fitting import ParamsHist 6 | from pytest import fixture 7 | 8 | 9 | @fixture 10 | def structure(): 11 | """Build a structure""" 12 | params = ParamsHist() 13 | params.add("SiO2_n0", value=1.452, min=-100, max=100, vary=False) 14 | params.add("SiO2_n1", value=36.0, min=-40000, max=40000, vary=False) 15 | params.add("SiO2_n2", value=0, min=-40000, max=40000, vary=False) 16 | params.add("SiO2_k0", value=0, min=-100, max=100, vary=False) 17 | params.add("SiO2_k1", value=0, min=-40000, max=40000, vary=False) 18 | params.add("SiO2_k2", value=0, min=-40000, max=40000, vary=False) 19 | params.add("SiO2_d", value=276.36, min=0, max=40000, vary=False) 20 | 21 | params.add("TiO2_n0", value=2.236, min=-100, max=100, vary=True) 22 | params.add("TiO2_n1", value=451, min=-40000, max=40000, vary=True) 23 | params.add("TiO2_n2", value=251, min=-40000, max=40000, vary=True) 24 | params.add("TiO2_k0", value=0, min=-100, max=100, vary=False) 25 | params.add("TiO2_k1", value=0, min=-40000, max=40000, vary=False) 26 | params.add("TiO2_k2", value=0, min=-40000, max=40000, vary=False) 27 | 28 | params.add("TiO2_d", value=20, min=0, max=40000, vary=True) 29 | 30 | SiO2 = elli.Cauchy( 31 | params["SiO2_n0"], 32 | params["SiO2_n1"], 33 | params["SiO2_n2"], 34 | params["SiO2_k0"], 35 | params["SiO2_k1"], 36 | params["SiO2_k2"], 37 | ).get_mat() 38 | TiO2 = elli.Cauchy( 39 | params["TiO2_n0"], 40 | params["TiO2_n1"], 41 | params["TiO2_n2"], 42 | params["TiO2_k0"], 43 | params["TiO2_k1"], 44 | params["TiO2_k2"], 45 | ).get_mat() 46 | 47 | Layer = [ 48 | elli.Layer(TiO2, params["TiO2_d"]), 49 | elli.Layer(SiO2, params["SiO2_d"]), 50 | elli.Layer(TiO2, params["TiO2_d"]), 51 | elli.Layer(SiO2, params["SiO2_d"]), 52 | elli.Layer(TiO2, params["TiO2_d"]), 53 | elli.Layer(SiO2, params["SiO2_d"]), 54 | elli.Layer(TiO2, params["TiO2_d"]), 55 | elli.Layer(SiO2, params["SiO2_d"]), 56 | ] 57 | 58 | return elli.Structure(elli.AIR, Layer, elli.AIR) 59 | 60 | 61 | lbda = np.linspace(400, 800, 500) 62 | PHI = 70 63 | 64 | 65 | def test_solver4x4_eig(benchmark, structure): 66 | """Benchmarks eignvalue propagator with solver4x4""" 67 | benchmark.pedantic( 68 | structure.evaluate, 69 | args=(lbda, PHI), 70 | kwargs={"solver": elli.Solver4x4, "propagator": elli.PropagatorEig()}, 71 | iterations=1, 72 | rounds=10, 73 | ) 74 | 75 | 76 | def test_solver4x4_expm(benchmark, structure): 77 | """Benchmarks expm-scipy propagator with solver4x4""" 78 | benchmark.pedantic( 79 | structure.evaluate, 80 | args=(lbda, PHI), 81 | kwargs={ 82 | "solver": elli.Solver4x4, 83 | "propagator": elli.PropagatorExpm(backend="scipy"), 84 | }, 85 | iterations=1, 86 | rounds=10, 87 | ) 88 | 89 | 90 | def test_solver4x4_expm_pytorch(benchmark, structure): 91 | """Benchmarks expm-torch propagator with solver4x4""" 92 | benchmark.pedantic( 93 | structure.evaluate, 94 | args=(lbda, PHI), 95 | kwargs={ 96 | "solver": elli.Solver4x4, 97 | "propagator": elli.PropagatorExpm(backend="torch"), 98 | }, 99 | iterations=1, 100 | rounds=10, 101 | ) 102 | 103 | 104 | def test_solver4x4_linear(benchmark, structure): 105 | """Benchmarks linear propagator with solver4x4""" 106 | benchmark.pedantic( 107 | structure.evaluate, 108 | args=(lbda, PHI), 109 | kwargs={"solver": elli.Solver4x4, "propagator": elli.PropagatorLinear()}, 110 | iterations=1, 111 | rounds=10, 112 | ) 113 | 114 | 115 | def test_solver2x2(benchmark, structure): 116 | """Benchmarks solver2x2""" 117 | benchmark.pedantic( 118 | structure.evaluate, 119 | args=(lbda, PHI), 120 | kwargs={"solver": elli.Solver2x2}, 121 | iterations=1, 122 | rounds=10, 123 | ) 124 | -------------------------------------------------------------------------------- /tests/create_dispersion_prototypes.py: -------------------------------------------------------------------------------- 1 | """This files generates files for the current values of the dispersions""" 2 | 3 | import elli 4 | import numpy as np 5 | 6 | 7 | def execute_and_save(dispersion, identifier, rep_params, *args, **kwargs): 8 | lbda = np.linspace(400, 1000, 500) 9 | fname = f"test_dispersions/{dispersion}_{identifier}.csv" 10 | 11 | disp = elli.DispersionFactory.get_dispersion(dispersion, *args, **kwargs) 12 | for rep_param in rep_params: 13 | disp.add(**rep_param) 14 | disp.get_dielectric_df(lbda).to_csv(fname) 15 | 16 | 17 | def loop_dispersions_default(): 18 | dispersions = [ 19 | "Cauchy", 20 | "CauchyUrbach", 21 | "DrudeEnergy", 22 | "DrudeResistivity", 23 | "Gaussian", 24 | "LorentzEnergy", 25 | "LorentzLambda", 26 | "Poles", 27 | "Sellmeier", 28 | "Tanguy", 29 | "TaucLorentz", 30 | "Table", 31 | "TableEpsilon", 32 | ] 33 | 34 | for dispersion in dispersions: 35 | execute_and_save(dispersion, "default", []) 36 | 37 | 38 | def loop_dispersions_custom_values(): 39 | dispersions = [ 40 | { 41 | "name": "Cauchy", 42 | "single_params": { 43 | "n0": 1.5, 44 | "n1": 0.3, 45 | "n2": 0.05, 46 | "k0": 0.6, 47 | "k1": 0.2, 48 | "k2": 0.1, 49 | }, 50 | "rep_params": [], 51 | }, 52 | { 53 | "name": "CauchyUrbach", 54 | "single_params": { 55 | "n0": 1.5, 56 | "B": 0.005, 57 | "C": 0.0001, 58 | "D": 0.0001, 59 | "Eg": 3, 60 | "Eu": 0.4, 61 | }, 62 | "rep_params": [], 63 | }, 64 | { 65 | "name": "DrudeEnergy", 66 | "single_params": {"A": 100, "gamma": 0.5}, 67 | "rep_params": [], 68 | }, 69 | { 70 | "name": "DrudeResistivity", 71 | "single_params": {"rho_opt": 100, "tau": 1e-2}, 72 | "rep_params": [], 73 | }, 74 | { 75 | "name": "LorentzLambda", 76 | "single_params": {}, 77 | "rep_params": [ 78 | {"A": 100, "lambda_r": 500, "gamma": 10}, 79 | {"A": 150, "lambda_r": 300, "gamma": 20}, 80 | {"A": 300, "lambda_r": 750, "gamma": 50}, 81 | ], 82 | }, 83 | { 84 | "name": "LorentzEnergy", 85 | "single_params": {}, 86 | "rep_params": [ 87 | {"A": 100, "E": 3, "gamma": 0.1}, 88 | {"A": 150, "E": 1.5, "gamma": 0.05}, 89 | {"A": 300, "E": 0.3, "gamma": 0.02}, 90 | ], 91 | }, 92 | { 93 | "name": "Gaussian", 94 | "single_params": {}, 95 | "rep_params": [ 96 | {"A": 100, "E": 3, "sigma": 0.1}, 97 | {"A": 150, "E": 1.5, "sigma": 0.05}, 98 | {"A": 300, "E": 0.3, "sigma": 0.02}, 99 | ], 100 | }, 101 | { 102 | "name": "TaucLorentz", 103 | "single_params": {"Eg": 2}, 104 | "rep_params": [ 105 | {"A": 100, "E": 2.5, "C": 0.1}, 106 | {"A": 150, "E": 3, "C": 0.05}, 107 | {"A": 300, "E": 4.5, "C": 0.02}, 108 | ], 109 | }, 110 | { 111 | "name": "Tanguy", 112 | "single_params": { 113 | "A": 1, 114 | "d": 2, 115 | "gamma": 0.1, 116 | "R": 0.1, 117 | "Eg": 2, 118 | "a": 1, 119 | "b": 0, 120 | }, 121 | "rep_params": [], 122 | }, 123 | { 124 | "name": "Poles", 125 | "single_params": {"A_ir": 100, "A_uv": 100, "E_uv": 4}, 126 | "rep_params": [], 127 | }, 128 | { 129 | "name": "Table", 130 | "single_params": { 131 | "lbda": np.linspace(400, 1000, 100), 132 | "n": np.linspace(1, 1.5, 100) + 1j * np.linspace(0, 1, 100), 133 | }, 134 | "rep_params": [], 135 | }, 136 | { 137 | "name": "TableEpsilon", 138 | "single_params": { 139 | "lbda": np.linspace(400, 1000, 100), 140 | "epsilon": np.linspace(1, 1.5, 100) + 1j * np.linspace(0, 1, 100), 141 | }, 142 | "rep_params": [], 143 | }, 144 | { 145 | "name": "PseudoDielectricFunction", 146 | "single_params": { 147 | "angle": 70, 148 | "lbda": np.linspace(400, 1000, 100), 149 | "psi": np.linspace(0, 90, 100), 150 | "delta": np.linspace(0, 90, 100), 151 | }, 152 | "rep_params": [], 153 | }, 154 | ] 155 | 156 | for dispersion in dispersions: 157 | execute_and_save( 158 | dispersion.get("name"), 159 | "custom_values", 160 | dispersion.get("rep_params"), 161 | **dispersion.get("single_params"), 162 | ) 163 | 164 | 165 | if __name__ == "__main__": 166 | loop_dispersions_default() 167 | loop_dispersions_custom_values() 168 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | from shutil import copytree, rmtree 3 | 4 | from pytest import fixture 5 | 6 | 7 | @fixture 8 | def datadir(tmp_path, request): 9 | """ 10 | Fixture responsible for searching a folder with the same name of test 11 | module and, if available, moving all contents to a temporary directory so 12 | tests can use them freely. 13 | """ 14 | filename = request.module.__file__ 15 | test_dir, _ = os.path.splitext(filename) 16 | 17 | if os.path.isdir(test_dir): 18 | rmtree(tmp_path) 19 | copytree(test_dir, str(tmp_path)) 20 | 21 | return tmp_path 22 | -------------------------------------------------------------------------------- /tests/test_accurion.py: -------------------------------------------------------------------------------- 1 | """Tests for reading accurion files""" 2 | 3 | import pytest 4 | from fixtures import datadir # pylint: disable=unused-import 5 | 6 | import elli 7 | 8 | 9 | # pylint: disable=redefined-outer-name 10 | def test_reading_of_psi_delta_woollam(datadir): 11 | """Psi/delta Accurion file is read w/o errors""" 12 | data = elli.read_accurion_psi_delta( 13 | datadir / "Si3N4_on_4inBF33_W02_20240903-150451.ds.dat" 14 | ) 15 | 16 | assert data.shape == (114, 2) 17 | -------------------------------------------------------------------------------- /tests/test_accurion/Si3N4_on_4inBF33_W02_20240903-150451.ds.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/tests/test_accurion/Si3N4_on_4inBF33_W02_20240903-150451.ds.dat -------------------------------------------------------------------------------- /tests/test_dispersion_factory.py: -------------------------------------------------------------------------------- 1 | """Testing cases for the dispersion law factory class.""" 2 | 3 | import elli 4 | import pytest 5 | 6 | 7 | def test_correct_object_generation(): 8 | """Factory class creates a correct object""" 9 | factory_disp_less = elli.DispersionFactory.get_dispersion( 10 | "ConstantRefractiveIndex", n=1.5 11 | ) 12 | 13 | assert isinstance(factory_disp_less, elli.ConstantRefractiveIndex) 14 | assert factory_disp_less.single_params.get("n") == 1.5 15 | 16 | 17 | def test_error_on_not_existing_class(): 18 | """Raises an error if dispersion does not exist.""" 19 | with pytest.raises(ValueError): 20 | elli.DispersionFactory.get_dispersion("DispersionNotExisting", n=1000) 21 | 22 | 23 | def test_error_on_bad_class(): 24 | """Raises an error if a bad class is requested.""" 25 | for bad_class in ["DispersionFactory", "Dispersion", "DispersionSum"]: 26 | with pytest.raises(ValueError): 27 | elli.DispersionFactory.get_dispersion(bad_class) 28 | -------------------------------------------------------------------------------- /tests/test_formula_dispersion.py: -------------------------------------------------------------------------------- 1 | """Tests for the formula dispersion""" 2 | 3 | import numpy as np 4 | from numpy.testing import assert_array_almost_equal 5 | import pytest 6 | 7 | from benchmark_formula_dispersion import structure as formula_structure 8 | from benchmark_propagators_TiO2 import structure 9 | import elli 10 | from elli.dispersions import Sellmeier, Formula 11 | from elli.dispersions.cauchy import Cauchy 12 | from elli.dispersions.formula import FormulaIndex 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "ref_model, formula_model, formula, single_params, rep_params, unit", 17 | [ 18 | ( 19 | Sellmeier, 20 | Formula, 21 | "eps = 1 + sum[A * (lbda * 1e-3)**2 / ((lbda * 1e-3) ** 2 - B)]", 22 | {}, 23 | {"A": [1, 1, 1], "B": [0.1, 0.1, 0.1]}, 24 | "nm", 25 | ), 26 | ( 27 | Sellmeier, 28 | Formula, 29 | "eps = 1 + sum[A * lbda**2 / (lbda** 2 - B)]", 30 | {}, 31 | {"A": [1, 1, 1], "B": [0.1, 0.1, 0.1]}, 32 | "micrometer", 33 | ), 34 | ( 35 | Cauchy, 36 | FormulaIndex, 37 | ( 38 | "n = n0 + 1e2 * n1 / lbda ** 2 + 1e7 * n2 / lbda ** 4 + " 39 | "1j * (k0 + 1e2 * k1 / lbda ** 2 + 1e7 * k2 / lbda ** 4)" 40 | ), 41 | {"n0": 1.5, "n1": 1, "n2": 1, "k0": 1, "k1": 1, "k2": 1}, 42 | {}, 43 | "nm", 44 | ), 45 | ], 46 | ) 47 | def test_formula_reproduces_predefined( 48 | ref_model, formula_model, formula, single_params, rep_params, unit 49 | ): 50 | """The formula dispersion reproduces other dispersion models""" 51 | lbda = np.linspace(400, 1500, 500) 52 | 53 | disp = ref_model(**single_params) 54 | for params in zip(*rep_params.values()): 55 | disp.add(*params) 56 | 57 | formula_disp = formula_model(formula, "lbda", single_params, rep_params, unit) 58 | 59 | assert_array_almost_equal( 60 | disp.get_dielectric(lbda), formula_disp.get_dielectric(lbda) 61 | ) 62 | 63 | 64 | def test_formula_fails_on_wrong_repr(): 65 | """The formula dispersion fails when it is tried to be 66 | initialized with the wrong represenation""" 67 | 68 | with pytest.raises(ValueError): 69 | Formula("n = 1", "", {}, {}) 70 | 71 | with pytest.raises(ValueError): 72 | FormulaIndex("eps = 1", "", {}, {}) 73 | 74 | 75 | def test_formula_against_predefined_model(structure, formula_structure): 76 | """A formula dispersion evaluates to the same values as a predefined model""" 77 | lbda = np.linspace(400, 800, 500) 78 | PHI = 70 79 | 80 | predefined4x4 = structure.evaluate( 81 | lbda, PHI, solver=elli.Solver4x4, propagator=elli.PropagatorExpm() 82 | ) 83 | formula4x4 = formula_structure.evaluate( 84 | lbda, PHI, solver=elli.Solver4x4, propagator=elli.PropagatorExpm() 85 | ) 86 | 87 | assert_array_almost_equal(predefined4x4.rho, formula4x4.rho) 88 | 89 | predefined2x2 = structure.evaluate(lbda, PHI, solver=elli.Solver2x2) 90 | formula2x2 = formula_structure.evaluate(lbda, PHI, solver=elli.Solver2x2) 91 | 92 | assert_array_almost_equal(predefined2x2.rho, formula2x2.rho) 93 | -------------------------------------------------------------------------------- /tests/test_kkr.py: -------------------------------------------------------------------------------- 1 | """Test Kramers Kronig relations""" 2 | 3 | import elli 4 | import numpy as np 5 | from elli.kkr import im2re, im2re_reciprocal 6 | from numpy.testing import assert_array_almost_equal 7 | 8 | 9 | def test_tauc_lorentz(): 10 | """Test whether the kkr reproduces the analytical expression of Tauc-Lorentz""" 11 | lbda = np.linspace(1e-2, 2000, 2000) 12 | g = elli.TaucLorentz(Eg=5).add(A=20, E=8, C=5) 13 | assert_array_almost_equal( 14 | im2re_reciprocal(g.get_dielectric(lbda).imag, lbda), 15 | g.get_dielectric(lbda).real, 16 | decimal=6, 17 | ) 18 | 19 | 20 | def test_tauc_lorentz_energy(): 21 | """Test whether the kkr in non reciprocal formulation reproduces the analytical expression 22 | of Tauc-Lorentz""" 23 | energy = np.linspace(0, 10, 5000) 24 | amp = 20 25 | osc_energy = 5 26 | gamma = 0.1 27 | lorentz = amp / (osc_energy**2 - energy**2 - 1j * gamma * energy) 28 | 29 | assert_array_almost_equal( 30 | im2re(lorentz.imag, energy)[10:], lorentz.real[10:], decimal=2 31 | ) 32 | 33 | 34 | def test_lorentz(): 35 | """Test whether the kkr reproduces the analytical expression of a Lorentz oscillator""" 36 | lbda = np.linspace(1e-2, 5000, 5000) 37 | g = elli.LorentzEnergy().add(A=20, E=5, gamma=5) 38 | assert_array_almost_equal( 39 | 1 + im2re_reciprocal(g.get_dielectric(lbda).imag, lbda)[:-1000], 40 | g.get_dielectric(lbda).real[:-1000], 41 | decimal=2, 42 | ) 43 | 44 | 45 | def test_gauss(): 46 | """KKR reproduces the analytical expression of gaussian.""" 47 | lbda = np.linspace(1e-2, 5000, 5000) 48 | g = elli.Gaussian().add(A=10, E=8, sigma=5) 49 | assert_array_almost_equal( 50 | im2re_reciprocal(g.get_dielectric(lbda).imag, lbda)[:-1000], 51 | g.get_dielectric(lbda).real[:-1000], 52 | decimal=2, 53 | ) 54 | -------------------------------------------------------------------------------- /tests/test_liquid_crystals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | 4 | import elli 5 | import numpy as np 6 | from numpy.lib.scimath import sqrt 7 | from scipy.constants import pi 8 | from scipy.signal import argrelmax, argrelmin 9 | 10 | 11 | def test_cholesteric_lc(): 12 | # Materials 13 | front = back = elli.IsotropicMaterial(elli.ConstantRefractiveIndex(n=1.6)) 14 | 15 | # Liquid crystal oriented along the x direction 16 | (no, ne) = (1.5, 1.7) 17 | Dn = ne - no 18 | n_med = (ne + no) / 2 19 | LC = elli.UniaxialMaterial( 20 | elli.ConstantRefractiveIndex(n=no), elli.ConstantRefractiveIndex(n=ne) 21 | ) # ne along z 22 | R = elli.rotation_v_theta(elli.E_Y, 90) # rotation round y 23 | LC.set_rotation(R) # apply rotation from z to x 24 | # Cholesteric pitch: 25 | p = 650 26 | # One half turn of a right-handed helix: 27 | TN = elli.TwistedLayer(LC, p / 2, 25, 180) 28 | 29 | # Inhomogeneous layer, repeated layer, and structure 30 | N = 5 # number half pitch repetitions 31 | h = N * p / 2 32 | L = elli.RepeatedLayers([TN], N) 33 | s = elli.Structure(front, [L], back) 34 | 35 | # Normal incidence: 36 | Kx = 0.0 37 | 38 | # Calculation parameters 39 | lbda_min, lbda_max = 600, 1500 # (nm) 40 | lbda = np.linspace(lbda_min, lbda_max, 100) 41 | k0 = 2 * pi / (lbda * 1e-9) 42 | 43 | # Theoretical calculation 44 | q = 2 * pi / p / 1e-9 45 | alpha = q / k0 46 | epsilon = (no**2 + ne**2) / 2 47 | delta = (no**2 - ne**2) / 2 48 | n2 = sqrt((alpha**2 + epsilon - sqrt(4 * epsilon * alpha**2 + delta**2))) 49 | w = 1j * (ne**2 - n2**2 - alpha**2) / (2 * alpha * n2) # not k0/c 50 | A = -2j * k0 * n2 * h * 1e-9 51 | 52 | R_th = ( 53 | np.abs( 54 | (w**2 + 1) 55 | * (1 - np.exp(-2j * k0 * n2 * h * 1e-9)) 56 | / ( 57 | 2 * w * (1 + np.exp(-2j * k0 * n2 * h * 1e-9)) 58 | - 1j * (w**2 - 1) * (1 - np.exp(-2j * k0 * n2 * h * 1e-9)) 59 | ) 60 | ) 61 | ** 2 62 | ) 63 | 64 | # Berreman simulation 65 | data = s.evaluate(lbda, 0) 66 | R_RR = data.Rc_RR 67 | 68 | # Checks positions of local extrema 69 | np.testing.assert_allclose(argrelmax(R_RR)[0], argrelmax(R_th)[0], atol=3) 70 | np.testing.assert_allclose(argrelmin(R_RR)[0], argrelmin(R_th)[0]) 71 | np.testing.assert_array_almost_equal(R_RR, R_th, decimal=1) 72 | 73 | 74 | def test_twisted_nematic_lc(): 75 | # Materials 76 | glass = elli.IsotropicMaterial(elli.ConstantRefractiveIndex(n=1.55)) 77 | front = back = glass 78 | 79 | # Liquid crystal oriented along the x direction 80 | (no, ne) = (1.5, 1.6) 81 | Dn = ne - no 82 | LC = elli.UniaxialMaterial( 83 | elli.ConstantRefractiveIndex(n=no), elli.ConstantRefractiveIndex(n=ne) 84 | ) 85 | R = elli.rotation_v_theta(elli.E_Y, 90) 86 | LC.set_rotation(R) 87 | d = 4330 88 | TN = elli.TwistedLayer(LC, d, 18, 90) 89 | 90 | # Structure 91 | s = elli.Structure(front, [TN], back) 92 | 93 | # Calculation parameters 94 | (lbda_min, lbda_max) = (200e-9, 1) # (m) 95 | k0_list = np.linspace(2 * pi / lbda_max, 2 * pi / lbda_min) 96 | lbda_list = (2 * pi) / k0_list * 1e9 97 | 98 | # Gooch-Tarry law 99 | u = 2 * d * Dn / lbda_list 100 | T_gt = np.sin(pi / 2 * sqrt(1 + u**2)) ** 2 / (1 + u**2) 101 | 102 | # Berreman simulation 103 | data = s.evaluate(lbda_list, 0) 104 | T_bm = data.T_pp 105 | 106 | # Compare results 107 | np.testing.assert_array_almost_equal(T_bm, T_gt, decimal=2) 108 | -------------------------------------------------------------------------------- /tests/test_materials.py: -------------------------------------------------------------------------------- 1 | """Tests for the materials classes""" 2 | 3 | import elli 4 | from pytest import raises 5 | 6 | 7 | class TestMaterials: 8 | disp = elli.ConstantRefractiveIndex(2) 9 | disp2 = elli.ConstantRefractiveIndex(5) 10 | mat = elli.IsotropicMaterial(disp) 11 | mat2 = elli.IsotropicMaterial(disp2) 12 | 13 | def test_materials_typeguard(self): 14 | """Checks if materials check input types""" 15 | with raises(TypeError): 16 | elli.IsotropicMaterial(self.mat) 17 | 18 | with raises(TypeError): 19 | elli.IsotropicMaterial(23) 20 | 21 | with raises(TypeError): 22 | elli.UniaxialMaterial(self.disp, 23) 23 | 24 | with raises(TypeError): 25 | elli.BiaxialMaterial(self.disp, self.disp, 23) 26 | 27 | with raises(TypeError): 28 | elli.VCAMaterial(self.mat, self.disp, 0.5) 29 | 30 | with raises(TypeError): 31 | elli.VCAMaterial(self.disp, self.mat, 0.5) 32 | 33 | with raises(ValueError): 34 | elli.VCAMaterial(self.mat, self.mat, 10) 35 | -------------------------------------------------------------------------------- /tests/test_mixture_models.py: -------------------------------------------------------------------------------- 1 | """Tests for the different mixture models""" 2 | 3 | import pytest 4 | import elli 5 | import numpy as np 6 | 7 | 8 | material_list = [ 9 | ("Si", "Aspnes"), 10 | ("GaAs", "Aspnes"), 11 | ("SiO2", "Malitson"), 12 | ("CaF2", "Li"), 13 | ("Rh", "Weaver"), 14 | ("MoS2", "Song-1L"), 15 | ] 16 | 17 | 18 | @pytest.fixture 19 | def RII(): 20 | RII = elli.db.RII() 21 | return RII 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "host_book, host_page", 26 | material_list, 27 | ) 28 | @pytest.mark.parametrize( 29 | "guest_book, guest_page", 30 | material_list, 31 | ) 32 | @pytest.mark.parametrize( 33 | "EMA", 34 | [ 35 | elli.LooyengaEMA, 36 | elli.MaxwellGarnettEMA, 37 | ], 38 | ) 39 | def test_mixture_models(host_book, host_page, guest_book, guest_page, EMA, RII): 40 | host_mat = RII.get_mat(host_book, host_page) 41 | guest_mat = RII.get_mat(guest_book, guest_page) 42 | 43 | lbda = np.arange(250, 800, 25) 44 | 45 | np.testing.assert_allclose( 46 | elli.BruggemanEMA(host_mat, guest_mat, 0.1).get_tensor(lbda), 47 | EMA(host_mat, guest_mat, 0.1).get_tensor(lbda), 48 | rtol=0.5, 49 | atol=0.5 + 0.5j, 50 | ) 51 | -------------------------------------------------------------------------------- /tests/test_nexus.py: -------------------------------------------------------------------------------- 1 | """Tests for a TiO2/SiO2/Si reference layer""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | import pytest 6 | 7 | from fixtures import datadir # pylint: disable=unused-import 8 | import elli 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "filename", 13 | ["ellips.test.nxs", "ellips_nx_opt.test.nxs"], 14 | ) 15 | # pylint: disable=redefined-outer-name 16 | def test_reading_of_psi_delta_nxs(datadir, filename): 17 | """Psi/delta NeXus file is read w/o errors""" 18 | elli.read_nexus_psi_delta(datadir / filename) 19 | 20 | 21 | @pytest.mark.parametrize( 22 | "filename", 23 | ["ellips.test.nxs", "ellips_nx_opt.test.nxs"], 24 | ) 25 | # pylint: disable=redefined-outer-name 26 | def test_reading_and_conv_to_rho(datadir, filename): 27 | """Rho values are read from Psi / Delta file""" 28 | elli.read_nexus_rho(datadir / filename) 29 | -------------------------------------------------------------------------------- /tests/test_nexus/ellips.test.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/tests/test_nexus/ellips.test.nxs -------------------------------------------------------------------------------- /tests/test_nexus/ellips_nx_opt.test.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/tests/test_nexus/ellips_nx_opt.test.nxs -------------------------------------------------------------------------------- /tests/test_nexus_formula/MoSe2-Munkhbat-o.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/tests/test_nexus_formula/MoSe2-Munkhbat-o.nxs -------------------------------------------------------------------------------- /tests/test_nexus_formula/MoTe2-Beal.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/tests/test_nexus_formula/MoTe2-Beal.nxs -------------------------------------------------------------------------------- /tests/test_nexus_formula/Si-Chandler-Horowitz.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/tests/test_nexus_formula/Si-Chandler-Horowitz.nxs -------------------------------------------------------------------------------- /tests/test_nexus_formula/Si-Salzberg.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/tests/test_nexus_formula/Si-Salzberg.nxs -------------------------------------------------------------------------------- /tests/test_nexus_formula/WTe2-Munkhbat-alpha.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/tests/test_nexus_formula/WTe2-Munkhbat-alpha.nxs -------------------------------------------------------------------------------- /tests/test_nexus_formula/YVO4-Shi-e-20C.nxs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyEllips/pyElli/49916db6b824e517a57003f5c2ca9c902362bc92/tests/test_nexus_formula/YVO4-Shi-e-20C.nxs -------------------------------------------------------------------------------- /tests/test_refractive_index_info.py: -------------------------------------------------------------------------------- 1 | """Tests the importing from the refractiveindex.info database.""" 2 | 3 | import elli 4 | import numpy as np 5 | import pytest 6 | 7 | 8 | class TestRefractiveIndexInfo: 9 | """Test if the refractive index.info parser works.""" 10 | 11 | RII = elli.db.RII() 12 | 13 | def test_get_mat(self): 14 | mat = self.RII.get_mat("Au", "Johnson") 15 | assert isinstance(mat, elli.IsotropicMaterial) 16 | 17 | def test_dispersion_error(self): 18 | with pytest.raises(ValueError): 19 | self.RII.get_dispersion("foo", "bar") 20 | 21 | def test_tabular_nk(self): 22 | disp = self.RII.get_dispersion("Au", "Johnson") # tabular nk 23 | 24 | np.testing.assert_almost_equal( 25 | disp.get_refractive_index(310.7), 1.53 + 1j * 1.893, decimal=3 26 | ) 27 | np.testing.assert_almost_equal( 28 | disp.get_refractive_index(1088), 0.27 + 1j * 7.150, decimal=3 29 | ) 30 | 31 | def test_tabular_n(self): 32 | disp = self.RII.get_dispersion("Xe", "Koch") # Tabular n 33 | 34 | np.testing.assert_almost_equal( 35 | disp.get_refractive_index(234.555), 1.00084664, decimal=6 36 | ) 37 | np.testing.assert_almost_equal( 38 | disp.get_refractive_index(612.327), 1.00070157, decimal=6 39 | ) 40 | 41 | def test_formula_1(self): 42 | disp = self.RII.get_dispersion("SrTiO3", "Dodge") # Formula 1 43 | 44 | np.testing.assert_almost_equal( 45 | disp.get_refractive_index(500), 2.4743, decimal=4 46 | ) 47 | np.testing.assert_almost_equal( 48 | disp.get_refractive_index(1000), 2.3160, decimal=4 49 | ) 50 | 51 | def test_formula_2_tabular_k(self): 52 | disp = self.RII.get_dispersion("SCHOTT-BK", "N-BK7") # Formula 2 + k 53 | 54 | np.testing.assert_almost_equal( 55 | disp.get_refractive_index(500), 1.5214 + 1j * 9.5781e-9, decimal=4 56 | ) 57 | np.testing.assert_almost_equal( 58 | disp.get_refractive_index(1000), 1.5075 + 1j * 9.9359e-9, decimal=4 59 | ) 60 | 61 | def test_formula_3(self): 62 | disp = self.RII.get_dispersion("HOYA-NbF", "NBF1") # Formula 3 63 | 64 | np.testing.assert_almost_equal( 65 | disp.get_refractive_index(500), 1.7520 + 1j * 3.9809e-9, decimal=4 66 | ) 67 | np.testing.assert_almost_equal( 68 | disp.get_refractive_index(1000), 1.7271 + 1j * 7.9617e-9, decimal=4 69 | ) 70 | 71 | def test_formula_4(self): 72 | disp = self.RII.get_dispersion("AgCl", "Tilton") # formula 4 73 | 74 | np.testing.assert_almost_equal( 75 | disp.get_refractive_index(1024), 2.0213900020277, decimal=12 76 | ) 77 | np.testing.assert_almost_equal( 78 | disp.get_refractive_index(10080), 1.9799748188746, decimal=12 79 | ) 80 | 81 | def test_formula_5(self): 82 | disp = self.RII.get_dispersion("SF6", "Vukovic") # Formula 5 83 | 84 | np.testing.assert_almost_equal( 85 | disp.get_refractive_index(600), 1.00072905, decimal=6 86 | ) 87 | np.testing.assert_almost_equal( 88 | disp.get_refractive_index(1000), 1.00072017, decimal=6 89 | ) 90 | 91 | def test_search(self): 92 | assert len(self.RII.search("")) == 0 93 | assert len(self.RII.search("C")) > len(self.RII.search("C", fuzzy=False)) 94 | assert len(self.RII.search("Au", "book")) > len( 95 | self.RII.search("Au", "book", fuzzy=False) 96 | ) 97 | -------------------------------------------------------------------------------- /tests/test_result.py: -------------------------------------------------------------------------------- 1 | """Tests for the result class""" 2 | 3 | import numpy as np 4 | from pytest import fixture, raises 5 | 6 | import elli 7 | 8 | 9 | @fixture 10 | def result(): 11 | """Generate a pyElli result object""" 12 | Si = elli.db.RII().get_mat("Si", "Aspnes") 13 | SiO2 = elli.Cauchy(1.452, 36.0).get_mat() 14 | structure = elli.Structure(elli.AIR, [elli.Layer(SiO2, 500)], Si) 15 | 16 | return structure.evaluate(np.linspace(250, 800), 70, solver=elli.Solver2x2) 17 | 18 | 19 | def test_delta_range_conversion(result): 20 | """Checks correct delta range conversion""" 21 | assert ( 22 | all(result.delta > -180) and all(result.delta < 180) and any(result.delta < 0) 23 | ) 24 | 25 | result.as_delta_range(0, 360) 26 | assert all(result.delta > 0) and all(result.delta < 360) 27 | 28 | result.as_delta_range(0, 180) 29 | assert all(result.delta > 0) and all(result.delta < 180) 30 | 31 | result.as_delta_range(-90, 270) 32 | assert all(result.delta > -90) and all(result.delta < 270) and any(result.delta < 0) 33 | 34 | result.as_delta_range(-180, 180) 35 | assert ( 36 | all(result.delta > -180) and all(result.delta < 180) and any(result.delta < 0) 37 | ) 38 | 39 | 40 | def test_resultlist_shape(result): 41 | result_list = elli.ResultList([result, result, result]) 42 | 43 | assert len(result_list) == 3 44 | assert np.shape(result_list.delta) == (3, 50) 45 | 46 | assert np.shape(result_list.mean.delta) == (50,) 47 | assert np.allclose(result_list.mean.delta, result.delta) 48 | -------------------------------------------------------------------------------- /tests/test_solvers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | import warnings 4 | 5 | import elli 6 | import numpy as np 7 | from pytest import raises 8 | 9 | 10 | def test_solver2x2_active_medium(): 11 | s = elli.Structure(elli.AIR, [], elli.ConstantRefractiveIndex(2 - 1j).get_mat()) 12 | with warnings.catch_warnings(record=True) as w: 13 | warnings.simplefilter("always") 14 | s.evaluate([200, 300, 400, 500], 70, solver=elli.Solver2x2) 15 | assert len(w) == 1 16 | assert issubclass(w[-1].category, UserWarning) 17 | 18 | 19 | def test_solver2x2_inverse_active_medium(): 20 | s = elli.Structure(elli.AIR, [], elli.ConstantRefractiveIndex(-2 + 1j).get_mat()) 21 | with warnings.catch_warnings(record=True) as w: 22 | warnings.simplefilter("always") 23 | s.evaluate([200, 300, 400, 500], 70, solver=elli.Solver2x2) 24 | assert len(w) == 1 25 | assert issubclass(w[-1].category, UserWarning) 26 | -------------------------------------------------------------------------------- /tests/test_spectraray.py: -------------------------------------------------------------------------------- 1 | """Tests for a TiO2/SiO2/Si reference layer""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | import pytest 6 | 7 | from fixtures import datadir # pylint: disable=unused-import 8 | import elli 9 | 10 | 11 | # pylint: disable=redefined-outer-name 12 | def test_reading_of_psi_delta_spetraray(datadir): 13 | """Psi/delta Spectraray file is read w/o errors""" 14 | elli.read_spectraray_psi_delta(datadir / "Si_SiO2_theta_50_60_70.txt") 15 | 16 | 17 | # pylint: disable=redefined-outer-name 18 | def test_reading_and_conv_to_spectraray(datadir): 19 | """Rho values are read from Psi / Delta file""" 20 | elli.read_spectraray_rho(datadir / "Si_SiO2_theta_50_60_70.txt") 21 | -------------------------------------------------------------------------------- /tests/test_structures.py: -------------------------------------------------------------------------------- 1 | """Tests for the result class""" 2 | 3 | import numpy as np 4 | import elli 5 | from pytest import raises 6 | 7 | 8 | class TestStructures: 9 | disp = elli.ConstantRefractiveIndex(2) 10 | mat = elli.IsotropicMaterial(disp) 11 | layer = elli.Layer(mat, 20) 12 | 13 | def test_layer_typeguard(self): 14 | """Test layer input checks.""" 15 | with raises(TypeError): 16 | elli.Layer(self.disp, 20) 17 | 18 | with raises(ValueError): 19 | elli.Layer(self.mat, -1) 20 | 21 | with raises(TypeError): 22 | elli.RepeatedLayers(self.layer, 2) 23 | 24 | with raises(TypeError): 25 | elli.RepeatedLayers([self.mat], 2) 26 | 27 | with raises(ValueError): 28 | elli.RepeatedLayers([self.layer], -2) 29 | 30 | with raises(ValueError): 31 | elli.RepeatedLayers([self.layer], 2, -2) 32 | 33 | with raises(ValueError): 34 | elli.RepeatedLayers([self.layer], 2, 0, -1) 35 | 36 | with raises(TypeError): 37 | elli.TwistedLayer(self.disp, 20, 5, 20) 38 | 39 | with raises(ValueError): 40 | elli.TwistedLayer(self.mat, -1, 5, 20) 41 | 42 | with raises(ValueError): 43 | elli.TwistedLayer(self.mat, 20, 0, 20) 44 | 45 | with raises(TypeError): 46 | elli.VaryingMixtureLayer(self.disp, 20, 5) 47 | 48 | with raises(TypeError): 49 | elli.VaryingMixtureLayer(self.mat, 20, 5) 50 | 51 | def test_structure_typeguard(self): 52 | """Test layer input checks.""" 53 | with raises(TypeError): 54 | elli.Structure(self.disp, [], self.mat) 55 | 56 | with raises(TypeError): 57 | elli.Structure(self.mat, [], self.disp) 58 | 59 | with raises(TypeError): 60 | elli.Structure(elli.AIR, self.layer, self.mat) 61 | 62 | with raises(TypeError): 63 | elli.Structure(elli.AIR, [self.mat], self.mat) 64 | 65 | def test_varying_mixture_layer(self): 66 | """Tests the basic functionality of the VML.""" 67 | vca_mat = elli.VCAMaterial(elli.AIR, self.mat, 0.5) 68 | vml = elli.VaryingMixtureLayer(vca_mat, 10, 3) 69 | 70 | np.testing.assert_array_equal( 71 | vml.get_permittivity_profile(500)[1][1], 72 | (elli.AIR.get_tensor(500) + self.mat.get_tensor(500)) / 2, 73 | ) 74 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """Tests for the utils file""" 2 | 3 | from pytest import raises 4 | 5 | import elli 6 | 7 | 8 | def test_delta_range_errors(): 9 | """Checks raising of errors on invalid range or type.""" 10 | with raises(TypeError): 11 | elli.DeltaRange("hallo", "welt") 12 | 13 | with raises(ValueError): 14 | elli.DeltaRange(20, 180) 15 | 16 | with raises(ValueError): 17 | elli.DeltaRange(0, 720) 18 | 19 | with raises(ValueError): 20 | elli.DeltaRange(360, 0) 21 | -------------------------------------------------------------------------------- /tests/test_wollam.py: -------------------------------------------------------------------------------- 1 | """Tests for reading woollam""" 2 | 3 | import pytest 4 | from fixtures import datadir # pylint: disable=unused-import 5 | import elli 6 | 7 | 8 | # pylint: disable=redefined-outer-name 9 | def test_reading_of_psi_delta_woollam(datadir): 10 | """Psi/delta Spectraray file is read w/o errors""" 11 | data_wvase = elli.read_woollam_psi_delta(datadir / "wvase_example.dat") 12 | data_cease = elli.read_woollam_psi_delta(datadir / "complete_ease_example.dat") 13 | 14 | assert data_wvase.shape == (542, 2) 15 | assert data_cease.shape == (3263, 2) 16 | 17 | 18 | # pylint: disable=redefined-outer-name 19 | def test_reading_and_conv_to_woollam(datadir): 20 | """Rho values are read from Psi / Delta file""" 21 | data_wvase = elli.read_woollam_rho(datadir / "wvase_example.dat") 22 | data_cease = elli.read_woollam_rho(datadir / "complete_ease_example.dat") 23 | 24 | assert data_wvase.shape == (542,) 25 | assert data_cease.shape == (3263,) 26 | 27 | 28 | # pylint: disable=redefined-outer-name 29 | def test_raises_not_implemented_for_tan_cos_format(datadir): 30 | """Raises error when a wvase Tan(Psi)/Cos(Delta) file is presented""" 31 | 32 | with pytest.raises(NotImplementedError): 33 | elli.read_woollam_psi_delta(datadir / "wvase_trig.dat") 34 | -------------------------------------------------------------------------------- /tests/test_wollam/wvase_trig.dat: -------------------------------------------------------------------------------- 1 | nm TRIG 2 | 300 70 .245 .123 3 | 400 70 .283 -.034 --------------------------------------------------------------------------------