PyOptik 3.0.5__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.github/workflows/deploy_PyPi.yml +3 -7
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.github/workflows/deploy_anaconda.yml +6 -6
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.github/workflows/deploy_coverage.yml +2 -1
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.github/workflows/deploy_documentation.yml +2 -2
- pyoptik-3.1.0/.github/workflows/quality.yml +18 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.github/workflows/tests.yml +2 -2
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.gitignore +5 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/CHANGELOG.md +20 -1
- pyoptik-3.1.0/CONTRIBUTING.md +10 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/Makefile +14 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PKG-INFO +94 -1
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/__init__.py +10 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/__main__.py +11 -1
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/_version.py +3 -3
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/catalog.py +4 -5
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/material/__init__.py +1 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/material/base_class.py +6 -1
- pyoptik-3.1.0/PyOptik/material/dataset.py +204 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/material/sellmeier_class.py +64 -33
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/material/tabulated_class.py +93 -39
- pyoptik-3.1.0/PyOptik/thin_film.py +279 -0
- pyoptik-3.1.0/PyOptik/tui.py +105 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik.egg-info/PKG-INFO +94 -1
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik.egg-info/SOURCES.txt +15 -2
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik.egg-info/requires.txt +3 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik.egg-info/scm_file_list.json +15 -2
- pyoptik-3.1.0/PyOptik.egg-info/scm_version.json +8 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/README.rst +91 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0/conda.recipe}/meta.yaml +1 -1
- pyoptik-3.1.0/docs/source/catalog_browser.rst +42 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/code.rst +42 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/conventions.rst +15 -0
- pyoptik-3.1.0/docs/source/custom_materials.rst +102 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/index.rst +3 -0
- pyoptik-3.1.0/docs/source/thin_films.rst +108 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/pyproject.toml +4 -0
- pyoptik-3.1.0/tests/test_datasets_and_custom_materials.py +106 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/tests/test_main_cli.py +13 -0
- pyoptik-3.1.0/tests/test_thin_film.py +124 -0
- pyoptik-3.1.0/tests/test_tui.py +11 -0
- pyoptik-3.1.0/tools/next_release_version.py +13 -0
- pyoptik-3.1.0/tools/release_tag.py +15 -0
- pyoptik-3.0.5/PyOptik.egg-info/scm_version.json +0 -8
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.coveragerc +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.flake8 +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.github/dependabot.yml +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/.pre-commit-config.yaml +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/LICENSE +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/directories.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/material_type.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik/utils.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik.egg-info/dependency_links.txt +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik.egg-info/entry_points.txt +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/PyOptik.egg-info/top_level.txt +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/Makefile +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/README.rst +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/group_properties/README.rst +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/group_properties/plot_group_properties.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/sellmeier/README.rst +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/sellmeier/plot_bk7.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/sellmeier/plot_compare_glasses.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/sellmeier/plot_silica.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/sellmeier/plot_water.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/tabulated/README.rst +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/tabulated/plot_polyethylene.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/tabulated/plot_silicon_nk.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/examples/tabulated/plot_silver.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/images/example_bk7.png +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/images/logo.svg +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/make.bat +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/_static/default.css +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/_static/favicon.png +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/_static/logo.svg +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/_static/thumbnail.png +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/conf.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/examples.rst +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/references.rst +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/docs/source/sg_execution_times.rst +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/pytest.ini +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/setup.cfg +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/tests/__init__.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/tests/conftest.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/tests/test_base_material_decorator.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/tests/test_catalog.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/tests/test_docstrings.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/tests/test_material_models.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/tests/test_utils_extra.py +0 -0
- {pyoptik-3.0.5 → pyoptik-3.1.0}/tests/test_validation_and_logging.py +0 -0
|
@@ -2,18 +2,14 @@ name: Deploy to PyPi
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
|
|
6
|
-
tags:
|
|
7
|
-
- '*'
|
|
8
|
-
pull_request:
|
|
9
|
-
branches: [ "master" ]
|
|
5
|
+
tags: [ "v*" ]
|
|
10
6
|
|
|
11
7
|
permissions:
|
|
12
|
-
|
|
8
|
+
contents: read
|
|
13
9
|
|
|
14
10
|
jobs:
|
|
15
11
|
pure_package:
|
|
16
|
-
uses: MartinPdeS/MPSActions/.github/workflows/publish_pure_package_to_PyPi.yml@
|
|
12
|
+
uses: MartinPdeS/MPSActions/.github/workflows/publish_pure_package_to_PyPi.yml@v5
|
|
17
13
|
with:
|
|
18
14
|
python-version: "3.11"
|
|
19
15
|
secrets:
|
|
@@ -2,20 +2,20 @@ name: Deploy Conda Packages
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
|
|
6
|
-
tags: '*'
|
|
7
|
-
pull_request:
|
|
8
|
-
branches: [ "master" ]
|
|
5
|
+
tags: [ "v*" ]
|
|
9
6
|
|
|
10
7
|
permissions:
|
|
11
|
-
|
|
8
|
+
contents: read
|
|
12
9
|
|
|
13
10
|
jobs:
|
|
14
11
|
linux:
|
|
15
|
-
uses: MartinPdeS/MPSActions/.github/workflows/publish_compiled_package_to_anaconda.yml@
|
|
12
|
+
uses: MartinPdeS/MPSActions/.github/workflows/publish_compiled_package_to_anaconda.yml@v5
|
|
16
13
|
with:
|
|
17
14
|
python_versions: '["3.13"]'
|
|
18
15
|
apt_package: ""
|
|
19
16
|
os_list: '["ubuntu-latest"]'
|
|
17
|
+
package_name: pyoptik
|
|
18
|
+
recipe_path: conda.recipe
|
|
19
|
+
test_import: PyOptik
|
|
20
20
|
secrets:
|
|
21
21
|
ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }}
|
|
@@ -17,9 +17,10 @@ permissions:
|
|
|
17
17
|
|
|
18
18
|
jobs:
|
|
19
19
|
ManyLinux_x86_64:
|
|
20
|
-
uses: MartinPdeS/MPSActions/.github/workflows/publish_coverage.yml@
|
|
20
|
+
uses: MartinPdeS/MPSActions/.github/workflows/publish_coverage.yml@v5
|
|
21
21
|
with:
|
|
22
22
|
python-version: "3.11"
|
|
23
23
|
apt-package: xvfb
|
|
24
|
+
coverage-package: PyOptik
|
|
24
25
|
secrets:
|
|
25
26
|
_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -16,8 +16,8 @@ permissions:
|
|
|
16
16
|
|
|
17
17
|
jobs:
|
|
18
18
|
ManyLinux_x86_64:
|
|
19
|
-
uses: MartinPdeS/MPSActions/.github/workflows/publish_documentation.yml@
|
|
19
|
+
uses: MartinPdeS/MPSActions/.github/workflows/publish_documentation.yml@v5
|
|
20
20
|
with:
|
|
21
21
|
python-version: "3.11"
|
|
22
22
|
package-name: "PyOptik"
|
|
23
|
-
apt-package: xvfb jq
|
|
23
|
+
apt-package: xvfb jq
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Static quality checks
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: ["master"]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: ["master"]
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
jobs:
|
|
10
|
+
flake8:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
|
|
14
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.11"
|
|
17
|
+
- run: python -m pip install "flake8==7.1.1"
|
|
18
|
+
- run: python -m flake8 PyOptik tests
|
|
@@ -15,8 +15,8 @@ jobs:
|
|
|
15
15
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
16
16
|
|
|
17
17
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
19
|
-
- uses: actions/setup-python@
|
|
18
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
|
|
19
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
|
20
20
|
with:
|
|
21
21
|
python-version: ${{ matrix.python-version }}
|
|
22
22
|
cache: pip
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
# Except ...
|
|
6
6
|
!.github
|
|
7
|
+
!conda.recipe
|
|
8
|
+
!conda.recipe/meta.yaml
|
|
7
9
|
!PyOptik
|
|
8
10
|
!development
|
|
9
11
|
!docs
|
|
10
12
|
!tests
|
|
13
|
+
!tools
|
|
14
|
+
!tools/*.py
|
|
11
15
|
!.flake8
|
|
12
16
|
!.gitignore
|
|
13
17
|
!.pre-commit-config.yaml
|
|
@@ -21,6 +25,7 @@
|
|
|
21
25
|
!pytest.ini
|
|
22
26
|
!.coveragerc
|
|
23
27
|
!CHANGELOG.md
|
|
28
|
+
!CONTRIBUTING.md
|
|
24
29
|
|
|
25
30
|
# But ignore docs/build inside docs and code
|
|
26
31
|
docs/build/
|
|
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.1.0] - 2026-09-16
|
|
11
|
+
|
|
10
12
|
### Added
|
|
11
13
|
|
|
12
14
|
- Support for all nine RefractiveIndex.INFO dispersion formula types.
|
|
@@ -25,6 +27,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
25
27
|
- Numerical regression, unit-equivalence, catalog-search, and cache-integrity
|
|
26
28
|
test coverage.
|
|
27
29
|
- A new PyOptik prism logo and documentation favicon.
|
|
30
|
+
- Typed, validated material documents for formula and tabulated optical data.
|
|
31
|
+
- User-defined material construction from arrays, CSV files, and formula
|
|
32
|
+
coefficients, with atomic RefractiveIndex.INFO-compatible YAML export.
|
|
33
|
+
- An optional Textual terminal browser for searching the local material
|
|
34
|
+
catalog and inspecting provenance.
|
|
35
|
+
- Fresnel reflection and transmission calculations for s and p polarization,
|
|
36
|
+
including Brewster and critical-angle helpers.
|
|
37
|
+
- Coherent transfer-matrix calculations for isotropic multilayer thin films,
|
|
38
|
+
supporting constant complex indices and wavelength-dependent PyOptik
|
|
39
|
+
material models.
|
|
40
|
+
- Numerical reference tests for interfaces, total internal reflection,
|
|
41
|
+
antireflection coatings, absorbing films, custom material round trips, and
|
|
42
|
+
malformed input paths.
|
|
28
43
|
|
|
29
44
|
### Changed
|
|
30
45
|
|
|
@@ -32,6 +47,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
32
47
|
GDD, `dτ_g/dω`, with time-squared units.
|
|
33
48
|
- Material plots use a consistent built-in Matplotlib layout and typography.
|
|
34
49
|
- Public catalog and group-delay APIs use expanded NumPy-style docstrings.
|
|
50
|
+
- Material YAML loading and export share one typed parser and validation layer.
|
|
51
|
+
- Wavelength validity endpoints tolerate floating-point round-off introduced
|
|
52
|
+
by unit conversions.
|
|
35
53
|
|
|
36
54
|
### Fixed
|
|
37
55
|
|
|
@@ -44,4 +62,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
44
62
|
plotting helpers.
|
|
45
63
|
- Retired logo assets.
|
|
46
64
|
|
|
47
|
-
[Unreleased]: https://github.com/MartinPdeS/PyOptik/compare/v3.
|
|
65
|
+
[Unreleased]: https://github.com/MartinPdeS/PyOptik/compare/v3.1.0...HEAD
|
|
66
|
+
[3.1.0]: https://github.com/MartinPdeS/PyOptik/compare/v3.0.5...v3.1.0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Contributing to PyOptik
|
|
2
|
+
|
|
3
|
+
Contributions should preserve PyOptik's offline material-catalogue workflow.
|
|
4
|
+
Add tests for behaviour changes, document data provenance and physical units,
|
|
5
|
+
and avoid committing generated assets, caches, or `PyOptik/_version.py`.
|
|
6
|
+
|
|
7
|
+
Use `make quality`, `make test`, and `make docs` as appropriate before a pull
|
|
8
|
+
request. Semantic release commands (`make release patch`, `minor`, or `major`)
|
|
9
|
+
create and push the release commit and exact tag; `make tag VERSION=vX.Y.Z`
|
|
10
|
+
creates a local-only tag.
|
|
@@ -3,10 +3,13 @@ BUILD_DIR ?= build
|
|
|
3
3
|
DIST_DIR ?= dist
|
|
4
4
|
DOCS_DIR ?= docs
|
|
5
5
|
DOCS_BUILD_DIR ?= $(DOCS_DIR)/build
|
|
6
|
+
RELEASE_KIND := $(filter major minor patch,$(MAKECMDGOALS))
|
|
6
7
|
|
|
7
8
|
.PHONY: help quality test docs build install uninstall quick rebuild editable \
|
|
8
9
|
setup download-all clean
|
|
9
10
|
|
|
11
|
+
.PHONY: release tag major minor patch
|
|
12
|
+
|
|
10
13
|
help:
|
|
11
14
|
@echo "PyOptik development targets:"
|
|
12
15
|
@echo " make quality Run static checks"
|
|
@@ -20,6 +23,7 @@ help:
|
|
|
20
23
|
@echo " make quick Run quality checks, tests, and build"
|
|
21
24
|
@echo " make rebuild Clean, then run the quick workflow"
|
|
22
25
|
@echo " make clean Remove generated build and test artifacts"
|
|
26
|
+
@echo " make release patch Create and push the next patch release"
|
|
23
27
|
|
|
24
28
|
quality:
|
|
25
29
|
$(PYTHON) -m flake8 PyOptik tests
|
|
@@ -55,6 +59,16 @@ setup:
|
|
|
55
59
|
download-all:
|
|
56
60
|
$(PYTHON) -m PyOptik download-all $(ARGS)
|
|
57
61
|
|
|
62
|
+
tag:
|
|
63
|
+
$(PYTHON) tools/release_tag.py "$(VERSION)"
|
|
64
|
+
|
|
65
|
+
release:
|
|
66
|
+
@test "$(words $(RELEASE_KIND))" -eq 1 || { echo "usage: make release [patch|minor|major]" >&2; exit 2; }
|
|
67
|
+
@set -eu; tag="$$($(PYTHON) tools/next_release_version.py $(RELEASE_KIND))"; $(PYTHON) tools/release_tag.py "$$tag"; git push origin HEAD "refs/tags/$$tag"
|
|
68
|
+
|
|
69
|
+
major minor patch:
|
|
70
|
+
@:
|
|
71
|
+
|
|
58
72
|
clean:
|
|
59
73
|
rm -rf $(BUILD_DIR) $(DIST_DIR) $(DOCS_BUILD_DIR) \
|
|
60
74
|
.pytest_cache htmlcov .coverage
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyOptik
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: A package for refractive index values.
|
|
5
5
|
Author-email: Martin Poinsinet de Sivry-Houle <martin.poinsinet.de.sivry@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -55,6 +55,8 @@ Requires-Dist: sphinx-rtd-theme==2.0.0; extra == "documentation"
|
|
|
55
55
|
Requires-Dist: pydata-sphinx-theme==0.14.1; extra == "documentation"
|
|
56
56
|
Provides-Extra: dev
|
|
57
57
|
Requires-Dist: flake8==7.1.1; extra == "dev"
|
|
58
|
+
Provides-Extra: ui
|
|
59
|
+
Requires-Dist: textual>=1.0; extra == "ui"
|
|
58
60
|
Dynamic: license-file
|
|
59
61
|
|
|
60
62
|
|logo|
|
|
@@ -101,6 +103,7 @@ Features
|
|
|
101
103
|
* Tabulated complex refractive index data, ``n + i k``.
|
|
102
104
|
* Unit-aware wavelength calculations through ``TypedUnit`` and Pint.
|
|
103
105
|
* Group index, group velocity, group delay, and group-delay dispersion.
|
|
106
|
+
* Fresnel interfaces and coherent multilayer thin-film calculations.
|
|
104
107
|
* NumPy-compatible scalar and array evaluation.
|
|
105
108
|
* Plotting helpers for dispersion and absorption data.
|
|
106
109
|
* Hierarchical catalog access using upstream ``shelf / book / page`` identity.
|
|
@@ -197,6 +200,35 @@ Use ``material.n(wavelength)``, ``material.k(wavelength)``,
|
|
|
197
200
|
See the documentation's physical-conventions page for the vacuum-wavelength,
|
|
198
201
|
``n + i k``, GDD, interpolation, and extrapolation conventions.
|
|
199
202
|
|
|
203
|
+
User-defined materials
|
|
204
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
205
|
+
|
|
206
|
+
Create tabulated materials directly from measured arrays or a CSV file, then
|
|
207
|
+
export them in the same validated YAML format used by the catalog:
|
|
208
|
+
|
|
209
|
+
.. code-block:: python
|
|
210
|
+
|
|
211
|
+
from TypedUnit import ureg
|
|
212
|
+
from PyOptik import TabulatedMaterial
|
|
213
|
+
|
|
214
|
+
sample = TabulatedMaterial.from_arrays(
|
|
215
|
+
"sample",
|
|
216
|
+
[400, 500, 600] * ureg.nanometer,
|
|
217
|
+
n=[1.40, 1.45, 1.50],
|
|
218
|
+
k=[0.01, 0.02, 0.04],
|
|
219
|
+
reference="Laboratory measurement",
|
|
220
|
+
)
|
|
221
|
+
sample.to_yaml("sample.yml")
|
|
222
|
+
|
|
223
|
+
# CSV headers default to: wavelength,n,k
|
|
224
|
+
imported = TabulatedMaterial.from_csv(
|
|
225
|
+
"sample.csv",
|
|
226
|
+
wavelength_unit=ureg.nanometer,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
Formula materials can be authored in the same way with
|
|
230
|
+
``SellmeierMaterial.from_coefficients(...)`` and exported with ``to_yaml()``.
|
|
231
|
+
|
|
200
232
|
Validity ranges
|
|
201
233
|
~~~~~~~~~~~~~~~
|
|
202
234
|
|
|
@@ -241,6 +273,55 @@ These methods accept scalar or array wavelengths and return unit-aware values.
|
|
|
241
273
|
``dτ_g/dω`` (typically expressed in fs²); use
|
|
242
274
|
``compute_group_delay_wavelength_slope`` for ``dτ_g/dλ``.
|
|
243
275
|
|
|
276
|
+
Interfaces and thin films
|
|
277
|
+
-------------------------
|
|
278
|
+
|
|
279
|
+
Calculate s- or p-polarized Fresnel coefficients at a single interface:
|
|
280
|
+
|
|
281
|
+
.. code-block:: python
|
|
282
|
+
|
|
283
|
+
from TypedUnit import ureg
|
|
284
|
+
from PyOptik import fresnel_coefficients, brewster_angle, critical_angle
|
|
285
|
+
|
|
286
|
+
interface = fresnel_coefficients(
|
|
287
|
+
1.0,
|
|
288
|
+
1.5,
|
|
289
|
+
angle=45 * ureg.degree,
|
|
290
|
+
polarization="p",
|
|
291
|
+
)
|
|
292
|
+
print(interface.reflectance, interface.transmittance)
|
|
293
|
+
|
|
294
|
+
print(brewster_angle(1.0, 1.5).to(ureg.degree))
|
|
295
|
+
print(critical_angle(1.5, 1.0).to(ureg.degree))
|
|
296
|
+
|
|
297
|
+
For coherent multilayers, supply constant indices or PyOptik material models.
|
|
298
|
+
Each thickness should carry units:
|
|
299
|
+
|
|
300
|
+
.. code-block:: python
|
|
301
|
+
|
|
302
|
+
import numpy
|
|
303
|
+
from PyOptik import ThinFilmLayer, thin_film_stack
|
|
304
|
+
|
|
305
|
+
wavelength = 600 * ureg.nanometer
|
|
306
|
+
substrate_index = 1.5
|
|
307
|
+
coating_index = numpy.sqrt(substrate_index)
|
|
308
|
+
coating = ThinFilmLayer(
|
|
309
|
+
coating_index,
|
|
310
|
+
wavelength / (4 * coating_index),
|
|
311
|
+
)
|
|
312
|
+
spectrum = thin_film_stack(
|
|
313
|
+
[500, 550, 600, 650, 700] * ureg.nanometer,
|
|
314
|
+
[coating],
|
|
315
|
+
incident_index=1.0,
|
|
316
|
+
substrate_index=substrate_index,
|
|
317
|
+
polarization="s",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
The returned results contain complex amplitude coefficients and the power
|
|
321
|
+
fractions ``reflectance``, ``transmittance``, and ``absorptance``. The model is
|
|
322
|
+
coherent and isotropic; it does not model roughness, anisotropy, or incoherent
|
|
323
|
+
thick substrates.
|
|
324
|
+
|
|
244
325
|
Plotting
|
|
245
326
|
--------
|
|
246
327
|
|
|
@@ -325,6 +406,18 @@ The command-line interface provides the canonical catalog workflow:
|
|
|
325
406
|
After installation, the equivalent console command is ``pyoptik
|
|
326
407
|
download-all --data-root ./refractiveindex-data``.
|
|
327
408
|
|
|
409
|
+
An optional terminal interface provides live search and a provenance pane for
|
|
410
|
+
browsing the downloaded database:
|
|
411
|
+
|
|
412
|
+
.. code-block:: bash
|
|
413
|
+
|
|
414
|
+
python -m pip install "PyOptik[ui]"
|
|
415
|
+
pyoptik setup
|
|
416
|
+
pyoptik browse
|
|
417
|
+
|
|
418
|
+
Use the arrow keys or mouse to select a page, ``/`` to focus search, and ``q``
|
|
419
|
+
to quit. Pass ``--data-root`` when browsing a non-default snapshot location.
|
|
420
|
+
|
|
328
421
|
For a beginner-friendly first-time setup, use:
|
|
329
422
|
|
|
330
423
|
.. code-block:: bash
|
|
@@ -14,6 +14,16 @@ from .catalog import MaterialCatalog, MaterialId, MaterialPage, download_snapsho
|
|
|
14
14
|
|
|
15
15
|
from .material import TabulatedMaterial
|
|
16
16
|
from .material import SellmeierMaterial
|
|
17
|
+
from .material import FormulaDataset, MaterialDocument, MaterialMetadata, TabulatedDataset, parse_material
|
|
18
|
+
from .thin_film import (
|
|
19
|
+
FresnelResult,
|
|
20
|
+
ThinFilmLayer,
|
|
21
|
+
ThinFilmResult,
|
|
22
|
+
brewster_angle,
|
|
23
|
+
critical_angle,
|
|
24
|
+
fresnel_coefficients,
|
|
25
|
+
thin_film_stack,
|
|
26
|
+
)
|
|
17
27
|
from .material import base_class
|
|
18
28
|
|
|
19
29
|
|
|
@@ -22,7 +22,7 @@ def main() -> None:
|
|
|
22
22
|
"command",
|
|
23
23
|
nargs="?",
|
|
24
24
|
default="setup",
|
|
25
|
-
choices=("setup", "download-all"),
|
|
25
|
+
choices=("setup", "download-all", "browse"),
|
|
26
26
|
help="Canonical catalog command (default: setup)",
|
|
27
27
|
)
|
|
28
28
|
parser.add_argument(
|
|
@@ -70,6 +70,16 @@ def main() -> None:
|
|
|
70
70
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
+
if args.command == "browse":
|
|
74
|
+
if args.force or args.fail_fast or args.source != "snapshot" or args.workers != 8 or args.no_progress:
|
|
75
|
+
parser.error("download options cannot be used with browse")
|
|
76
|
+
from PyOptik.tui import run_browser
|
|
77
|
+
try:
|
|
78
|
+
run_browser(args.data_root)
|
|
79
|
+
except (FileNotFoundError, RuntimeError) as error:
|
|
80
|
+
parser.error(str(error))
|
|
81
|
+
return
|
|
82
|
+
|
|
73
83
|
if args.command == "setup" and args.source != "snapshot":
|
|
74
84
|
parser.error("setup always uses the upstream snapshot; use download-all for page mode")
|
|
75
85
|
source = "snapshot" if args.command == "setup" else args.source
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '3.0
|
|
22
|
-
__version_tuple__ = version_tuple = (3,
|
|
21
|
+
__version__ = version = '3.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 1, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g705a5ec86'
|
|
@@ -145,17 +145,16 @@ class MaterialPage:
|
|
|
145
145
|
"""
|
|
146
146
|
from PyOptik.material.sellmeier_class import SellmeierMaterial
|
|
147
147
|
from PyOptik.material.tabulated_class import TabulatedMaterial
|
|
148
|
+
from PyOptik.material.dataset import parse_material
|
|
148
149
|
|
|
149
150
|
if self.local_path is not None and self.local_path.exists():
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
entries = document.get("DATA", [])
|
|
153
|
-
if any("formula" in str(entry.get("type", "")) for entry in entries):
|
|
151
|
+
document = parse_material(self.local_path)
|
|
152
|
+
if document.formula_datasets:
|
|
154
153
|
material = SellmeierMaterial(self.name, file_path=self.local_path)
|
|
155
154
|
material.catalog_id = self.id.key
|
|
156
155
|
material.source_url = self.source_url
|
|
157
156
|
return material
|
|
158
|
-
if
|
|
157
|
+
if document.tabulated_datasets:
|
|
159
158
|
material = TabulatedMaterial(
|
|
160
159
|
self.name,
|
|
161
160
|
file_path=self.local_path,
|
|
@@ -195,8 +195,13 @@ class BaseMaterial(object):
|
|
|
195
195
|
raise ValueError("out_of_range must be 'warn', 'raise', or 'clip'.")
|
|
196
196
|
if self.wavelength_bound is not None:
|
|
197
197
|
min_value, max_value = self.wavelength_bound
|
|
198
|
+
values = wavelength.to(ureg.meter).magnitude
|
|
199
|
+
lower = min_value.to(ureg.meter).magnitude
|
|
200
|
+
upper = max_value.to(ureg.meter).magnitude
|
|
201
|
+
below = (values < lower) & ~numpy.isclose(values, lower, rtol=1e-12, atol=0.0)
|
|
202
|
+
above = (values > upper) & ~numpy.isclose(values, upper, rtol=1e-12, atol=0.0)
|
|
198
203
|
|
|
199
|
-
if numpy.any(
|
|
204
|
+
if numpy.any(below | above):
|
|
200
205
|
message = (
|
|
201
206
|
f"Wavelength range goes from {wavelength.min().to_compact()} to {wavelength.max().to_compact()} "
|
|
202
207
|
f"which is outside the allowable range of {min_value.to_compact()} to {max_value.to_compact()} µm. "
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Typed, validated representations of PyOptik material datasets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping, Sequence
|
|
8
|
+
|
|
9
|
+
import numpy
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class MaterialMetadata:
|
|
15
|
+
"""Source metadata shared by formula and tabulated datasets."""
|
|
16
|
+
|
|
17
|
+
reference: str | None = None
|
|
18
|
+
conditions: Mapping[str, Any] = field(default_factory=dict)
|
|
19
|
+
comments: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class FormulaDataset:
|
|
24
|
+
"""A validated RefractiveIndex.INFO formula dataset."""
|
|
25
|
+
|
|
26
|
+
formula_type: int
|
|
27
|
+
coefficients: tuple[float, ...]
|
|
28
|
+
wavelength_range: tuple[float, float] | None = None
|
|
29
|
+
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
"""Validate formula identity, coefficients, and optional bounds."""
|
|
32
|
+
if self.formula_type not in range(1, 10):
|
|
33
|
+
raise ValueError(f"Unsupported formula type: {self.formula_type}")
|
|
34
|
+
if not self.coefficients or not numpy.all(numpy.isfinite(self.coefficients)):
|
|
35
|
+
raise ValueError("formula coefficients must be a non-empty finite sequence")
|
|
36
|
+
_validate_range(self.wavelength_range)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class TabulatedDataset:
|
|
41
|
+
"""One validated table of ``n``, ``k``, or combined ``nk`` values."""
|
|
42
|
+
|
|
43
|
+
kind: str
|
|
44
|
+
wavelength_um: tuple[float, ...]
|
|
45
|
+
values: tuple[tuple[float, ...], ...]
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
"""Validate shape, finiteness, and wavelength ordering."""
|
|
49
|
+
if self.kind not in {"n", "k", "nk"}:
|
|
50
|
+
raise ValueError("tabulated kind must be 'n', 'k', or 'nk'")
|
|
51
|
+
expected = 2 if self.kind == "nk" else 1
|
|
52
|
+
if len(self.wavelength_um) < 2 or len(self.values) != len(self.wavelength_um):
|
|
53
|
+
raise ValueError(f"tabulated {self.kind} data must contain at least two rows")
|
|
54
|
+
if any(len(row) != expected for row in self.values):
|
|
55
|
+
raise ValueError(f"tabulated {self.kind} rows require {expected + 1} columns")
|
|
56
|
+
wavelength = numpy.asarray(self.wavelength_um, dtype=float)
|
|
57
|
+
data = numpy.asarray(self.values, dtype=float)
|
|
58
|
+
if not numpy.all(numpy.isfinite(wavelength)) or not numpy.all(numpy.isfinite(data)):
|
|
59
|
+
raise ValueError("tabulated data must be finite")
|
|
60
|
+
if numpy.any(numpy.diff(wavelength) <= 0):
|
|
61
|
+
raise ValueError("tabulated wavelengths must be strictly increasing")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Dataset = FormulaDataset | TabulatedDataset
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class MaterialDocument:
|
|
69
|
+
"""A parsed optical-material document and its provenance metadata."""
|
|
70
|
+
|
|
71
|
+
datasets: tuple[Dataset, ...]
|
|
72
|
+
metadata: MaterialMetadata = field(default_factory=MaterialMetadata)
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
"""Require at least one supported dataset."""
|
|
76
|
+
if not self.datasets:
|
|
77
|
+
raise ValueError("material document contains no supported dataset")
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def formula_datasets(self) -> tuple[FormulaDataset, ...]:
|
|
81
|
+
"""Return formula datasets in source order."""
|
|
82
|
+
return tuple(item for item in self.datasets if isinstance(item, FormulaDataset))
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def tabulated_datasets(self) -> tuple[TabulatedDataset, ...]:
|
|
86
|
+
"""Return tabulated datasets in source order."""
|
|
87
|
+
return tuple(item for item in self.datasets if isinstance(item, TabulatedDataset))
|
|
88
|
+
|
|
89
|
+
def to_mapping(self) -> dict[str, Any]:
|
|
90
|
+
"""Return a RefractiveIndex.INFO-compatible YAML mapping."""
|
|
91
|
+
document: dict[str, Any] = {}
|
|
92
|
+
if self.metadata.reference is not None:
|
|
93
|
+
document["REFERENCES"] = self.metadata.reference
|
|
94
|
+
if self.metadata.comments is not None:
|
|
95
|
+
document["COMMENTS"] = self.metadata.comments
|
|
96
|
+
if self.metadata.conditions:
|
|
97
|
+
document["CONDITIONS"] = dict(self.metadata.conditions)
|
|
98
|
+
entries = []
|
|
99
|
+
for dataset in self.datasets:
|
|
100
|
+
if isinstance(dataset, FormulaDataset):
|
|
101
|
+
entry: dict[str, Any] = {
|
|
102
|
+
"type": f"formula {dataset.formula_type}",
|
|
103
|
+
"coefficients": " ".join(_format_number(value) for value in dataset.coefficients),
|
|
104
|
+
}
|
|
105
|
+
if dataset.wavelength_range is not None:
|
|
106
|
+
entry["wavelength_range"] = " ".join(
|
|
107
|
+
_format_number(value) for value in dataset.wavelength_range
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
rows = []
|
|
111
|
+
for wavelength, values in zip(dataset.wavelength_um, dataset.values):
|
|
112
|
+
rows.append(" ".join(_format_number(value) for value in (wavelength, *values)))
|
|
113
|
+
entry = {"type": f"tabulated {dataset.kind}", "data": "\n".join(rows) + "\n"}
|
|
114
|
+
entries.append(entry)
|
|
115
|
+
document["DATA"] = entries
|
|
116
|
+
return document
|
|
117
|
+
|
|
118
|
+
def to_yaml(self, path: str | Path) -> Path:
|
|
119
|
+
"""Serialize this document to ``path`` and return the resulting path."""
|
|
120
|
+
destination = Path(path)
|
|
121
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
temporary = destination.with_suffix(destination.suffix + ".tmp")
|
|
123
|
+
with temporary.open("w", encoding="utf-8") as stream:
|
|
124
|
+
yaml.safe_dump(self.to_mapping(), stream, sort_keys=False, allow_unicode=True)
|
|
125
|
+
temporary.replace(destination)
|
|
126
|
+
return destination
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_material(source: str | Path | Mapping[str, Any]) -> MaterialDocument:
|
|
130
|
+
"""Parse and validate a material YAML path or already-loaded mapping."""
|
|
131
|
+
if isinstance(source, Mapping):
|
|
132
|
+
raw = dict(source)
|
|
133
|
+
label = "material mapping"
|
|
134
|
+
else:
|
|
135
|
+
path = Path(source)
|
|
136
|
+
label = str(path)
|
|
137
|
+
try:
|
|
138
|
+
with path.open("r", encoding="utf-8") as stream:
|
|
139
|
+
raw = yaml.safe_load(stream)
|
|
140
|
+
except (OSError, yaml.YAMLError) as error:
|
|
141
|
+
raise ValueError(f"Unable to read material YAML {label}: {error}") from error
|
|
142
|
+
if not isinstance(raw, dict):
|
|
143
|
+
raise ValueError(f"Invalid material YAML in {label}: expected a mapping")
|
|
144
|
+
entries = raw.get("DATA")
|
|
145
|
+
if not isinstance(entries, list):
|
|
146
|
+
raise ValueError(f"Invalid material YAML in {label}: DATA must be a list")
|
|
147
|
+
datasets: list[Dataset] = []
|
|
148
|
+
try:
|
|
149
|
+
for entry in entries:
|
|
150
|
+
if not isinstance(entry, dict):
|
|
151
|
+
raise ValueError("DATA entries must be mappings")
|
|
152
|
+
tokens = str(entry.get("type", "")).lower().split()
|
|
153
|
+
if len(tokens) != 2:
|
|
154
|
+
continue
|
|
155
|
+
if tokens[0] == "formula":
|
|
156
|
+
wavelength_range = (
|
|
157
|
+
_numbers(entry["wavelength_range"])
|
|
158
|
+
if "wavelength_range" in entry else ()
|
|
159
|
+
)
|
|
160
|
+
datasets.append(FormulaDataset(
|
|
161
|
+
formula_type=int(tokens[1]),
|
|
162
|
+
coefficients=_numbers(entry["coefficients"]),
|
|
163
|
+
wavelength_range=tuple(wavelength_range) if wavelength_range else None,
|
|
164
|
+
))
|
|
165
|
+
elif tokens[0] == "tabulated" and tokens[1] in {"n", "k", "nk"}:
|
|
166
|
+
rows = tuple(_numbers(line) for line in str(entry["data"]).strip().splitlines())
|
|
167
|
+
datasets.append(TabulatedDataset(
|
|
168
|
+
kind=tokens[1],
|
|
169
|
+
wavelength_um=tuple(row[0] for row in rows),
|
|
170
|
+
values=tuple(tuple(row[1:]) for row in rows),
|
|
171
|
+
))
|
|
172
|
+
except (KeyError, TypeError, ValueError) as error:
|
|
173
|
+
raise ValueError(f"Invalid material dataset in {label}: {error}") from error
|
|
174
|
+
try:
|
|
175
|
+
metadata = MaterialMetadata(
|
|
176
|
+
reference=str(raw["REFERENCES"]) if raw.get("REFERENCES") is not None else None,
|
|
177
|
+
conditions=dict(raw.get("CONDITIONS") or {}),
|
|
178
|
+
comments=str(raw["COMMENTS"]) if raw.get("COMMENTS") is not None else None,
|
|
179
|
+
)
|
|
180
|
+
except (TypeError, ValueError) as error:
|
|
181
|
+
raise ValueError(f"Invalid material metadata in {label}: {error}") from error
|
|
182
|
+
try:
|
|
183
|
+
return MaterialDocument(tuple(datasets), metadata)
|
|
184
|
+
except ValueError as error:
|
|
185
|
+
raise ValueError(f"Invalid material dataset in {label}: {error}") from error
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _numbers(value: str | Sequence[float]) -> tuple[float, ...]:
|
|
189
|
+
"""Convert whitespace-delimited text or a sequence to floats."""
|
|
190
|
+
values = value.split() if isinstance(value, str) else value
|
|
191
|
+
return tuple(float(item) for item in values)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _validate_range(value: tuple[float, float] | None) -> None:
|
|
195
|
+
"""Validate an optional increasing two-value wavelength interval."""
|
|
196
|
+
if value is None:
|
|
197
|
+
return
|
|
198
|
+
if len(value) != 2 or not numpy.all(numpy.isfinite(value)) or value[0] >= value[1]:
|
|
199
|
+
raise ValueError("wavelength_range must contain two increasing finite values")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _format_number(value: float) -> str:
|
|
203
|
+
"""Format a float precisely enough for lossless YAML round trips."""
|
|
204
|
+
return format(float(value), ".17g")
|