PyOptik 3.0.4__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.
Files changed (90) hide show
  1. {pyoptik-3.0.4 → pyoptik-3.1.0}/.github/workflows/deploy_PyPi.yml +3 -7
  2. {pyoptik-3.0.4 → pyoptik-3.1.0}/.github/workflows/deploy_anaconda.yml +6 -6
  3. {pyoptik-3.0.4 → pyoptik-3.1.0}/.github/workflows/deploy_coverage.yml +2 -1
  4. {pyoptik-3.0.4 → pyoptik-3.1.0}/.github/workflows/deploy_documentation.yml +2 -2
  5. pyoptik-3.1.0/.github/workflows/quality.yml +18 -0
  6. {pyoptik-3.0.4 → pyoptik-3.1.0}/.github/workflows/tests.yml +2 -2
  7. {pyoptik-3.0.4 → pyoptik-3.1.0}/.gitignore +5 -0
  8. pyoptik-3.1.0/CHANGELOG.md +66 -0
  9. pyoptik-3.1.0/CONTRIBUTING.md +10 -0
  10. {pyoptik-3.0.4 → pyoptik-3.1.0}/Makefile +14 -0
  11. {pyoptik-3.0.4 → pyoptik-3.1.0}/PKG-INFO +100 -1
  12. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/__init__.py +10 -0
  13. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/__main__.py +11 -1
  14. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/_version.py +3 -3
  15. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/catalog.py +23 -8
  16. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/material/__init__.py +1 -0
  17. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/material/base_class.py +113 -1
  18. pyoptik-3.1.0/PyOptik/material/dataset.py +204 -0
  19. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/material/sellmeier_class.py +67 -31
  20. pyoptik-3.1.0/PyOptik/material/tabulated_class.py +342 -0
  21. pyoptik-3.1.0/PyOptik/thin_film.py +279 -0
  22. pyoptik-3.1.0/PyOptik/tui.py +105 -0
  23. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik.egg-info/PKG-INFO +100 -1
  24. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik.egg-info/SOURCES.txt +16 -2
  25. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik.egg-info/requires.txt +3 -0
  26. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik.egg-info/scm_file_list.json +16 -2
  27. pyoptik-3.1.0/PyOptik.egg-info/scm_version.json +8 -0
  28. {pyoptik-3.0.4 → pyoptik-3.1.0}/README.rst +97 -0
  29. {pyoptik-3.0.4 → pyoptik-3.1.0/conda.recipe}/meta.yaml +1 -1
  30. pyoptik-3.1.0/docs/source/catalog_browser.rst +42 -0
  31. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/code.rst +42 -0
  32. pyoptik-3.1.0/docs/source/conventions.rst +64 -0
  33. pyoptik-3.1.0/docs/source/custom_materials.rst +102 -0
  34. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/index.rst +4 -0
  35. pyoptik-3.1.0/docs/source/thin_films.rst +108 -0
  36. {pyoptik-3.0.4 → pyoptik-3.1.0}/pyproject.toml +4 -0
  37. pyoptik-3.1.0/tests/test_datasets_and_custom_materials.py +106 -0
  38. {pyoptik-3.0.4 → pyoptik-3.1.0}/tests/test_main_cli.py +13 -0
  39. {pyoptik-3.0.4 → pyoptik-3.1.0}/tests/test_material_models.py +24 -0
  40. pyoptik-3.1.0/tests/test_thin_film.py +124 -0
  41. pyoptik-3.1.0/tests/test_tui.py +11 -0
  42. pyoptik-3.1.0/tools/next_release_version.py +13 -0
  43. pyoptik-3.1.0/tools/release_tag.py +15 -0
  44. pyoptik-3.0.4/CHANGELOG.md +0 -42
  45. pyoptik-3.0.4/PyOptik/material/tabulated_class.py +0 -227
  46. pyoptik-3.0.4/PyOptik.egg-info/scm_version.json +0 -8
  47. {pyoptik-3.0.4 → pyoptik-3.1.0}/.coveragerc +0 -0
  48. {pyoptik-3.0.4 → pyoptik-3.1.0}/.flake8 +0 -0
  49. {pyoptik-3.0.4 → pyoptik-3.1.0}/.github/dependabot.yml +0 -0
  50. {pyoptik-3.0.4 → pyoptik-3.1.0}/.pre-commit-config.yaml +0 -0
  51. {pyoptik-3.0.4 → pyoptik-3.1.0}/LICENSE +0 -0
  52. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/directories.py +0 -0
  53. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/material_type.py +0 -0
  54. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik/utils.py +0 -0
  55. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik.egg-info/dependency_links.txt +0 -0
  56. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik.egg-info/entry_points.txt +0 -0
  57. {pyoptik-3.0.4 → pyoptik-3.1.0}/PyOptik.egg-info/top_level.txt +0 -0
  58. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/Makefile +0 -0
  59. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/README.rst +0 -0
  60. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/group_properties/README.rst +0 -0
  61. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/group_properties/plot_group_properties.py +0 -0
  62. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/sellmeier/README.rst +0 -0
  63. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/sellmeier/plot_bk7.py +0 -0
  64. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/sellmeier/plot_compare_glasses.py +0 -0
  65. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/sellmeier/plot_silica.py +0 -0
  66. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/sellmeier/plot_water.py +0 -0
  67. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/tabulated/README.rst +0 -0
  68. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/tabulated/plot_polyethylene.py +0 -0
  69. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/tabulated/plot_silicon_nk.py +0 -0
  70. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/examples/tabulated/plot_silver.py +0 -0
  71. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/images/example_bk7.png +0 -0
  72. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/images/logo.svg +0 -0
  73. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/make.bat +0 -0
  74. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/_static/default.css +0 -0
  75. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/_static/favicon.png +0 -0
  76. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/_static/logo.svg +0 -0
  77. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/_static/thumbnail.png +0 -0
  78. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/conf.py +0 -0
  79. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/examples.rst +0 -0
  80. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/references.rst +0 -0
  81. {pyoptik-3.0.4 → pyoptik-3.1.0}/docs/source/sg_execution_times.rst +0 -0
  82. {pyoptik-3.0.4 → pyoptik-3.1.0}/pytest.ini +0 -0
  83. {pyoptik-3.0.4 → pyoptik-3.1.0}/setup.cfg +0 -0
  84. {pyoptik-3.0.4 → pyoptik-3.1.0}/tests/__init__.py +0 -0
  85. {pyoptik-3.0.4 → pyoptik-3.1.0}/tests/conftest.py +0 -0
  86. {pyoptik-3.0.4 → pyoptik-3.1.0}/tests/test_base_material_decorator.py +0 -0
  87. {pyoptik-3.0.4 → pyoptik-3.1.0}/tests/test_catalog.py +0 -0
  88. {pyoptik-3.0.4 → pyoptik-3.1.0}/tests/test_docstrings.py +0 -0
  89. {pyoptik-3.0.4 → pyoptik-3.1.0}/tests/test_utils_extra.py +0 -0
  90. {pyoptik-3.0.4 → 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
- branches: [ "master" ]
6
- tags:
7
- - '*'
8
- pull_request:
9
- branches: [ "master" ]
5
+ tags: [ "v*" ]
10
6
 
11
7
  permissions:
12
- contents: write
8
+ contents: read
13
9
 
14
10
  jobs:
15
11
  pure_package:
16
- uses: MartinPdeS/MPSActions/.github/workflows/publish_pure_package_to_PyPi.yml@master
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
- branches: [ "master" ]
6
- tags: '*'
7
- pull_request:
8
- branches: [ "master" ]
5
+ tags: [ "v*" ]
9
6
 
10
7
  permissions:
11
- contents: write
8
+ contents: read
12
9
 
13
10
  jobs:
14
11
  linux:
15
- uses: MartinPdeS/MPSActions/.github/workflows/publish_compiled_package_to_anaconda.yml@master
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@master
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@master
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@v4
19
- - uses: actions/setup-python@v5
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/
@@ -0,0 +1,66 @@
1
+ # Changelog
2
+
3
+ All notable changes to PyOptik are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [3.1.0] - 2026-09-16
11
+
12
+ ### Added
13
+
14
+ - Support for all nine RefractiveIndex.INFO dispersion formula types.
15
+ - `MaterialCatalog.search()` with hierarchy, source, reference, and local
16
+ availability filters.
17
+ - `MaterialPage.provenance()` for serializable material-source records.
18
+ - `MaterialCatalog.verify_integrity()` for SHA-256 verification of cached
19
+ material data.
20
+ - `compute_group_delay_wavelength_slope()` for the explicit wavelength-space
21
+ derivative, `dτ_g/dλ`.
22
+ - Convenience APIs for real index, extinction coefficient, relative
23
+ permittivity, and absorption coefficient.
24
+ - Split tabulated ``n`` and ``k`` YAML support and opt-in monotonic PCHIP
25
+ interpolation.
26
+ - A physical conventions and provenance documentation guide.
27
+ - Numerical regression, unit-equivalence, catalog-search, and cache-integrity
28
+ test coverage.
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.
43
+
44
+ ### Changed
45
+
46
+ - `compute_group_delay_dispersion()` now returns conventional frequency-domain
47
+ GDD, `dτ_g/dω`, with time-squared units.
48
+ - Material plots use a consistent built-in Matplotlib layout and typography.
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.
53
+
54
+ ### Fixed
55
+
56
+ - Formula type 6 now accumulates every gas-dispersion term instead of
57
+ overwriting earlier terms.
58
+
59
+ ### Removed
60
+
61
+ - The `MPSPlots` runtime dependency and its use in examples and material
62
+ plotting helpers.
63
+ - Retired logo assets.
64
+
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.4
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.
@@ -191,6 +194,41 @@ Tabulated materials interpolate complex optical constants:
191
194
  The real part is the refractive index ``n`` and the imaginary part is the
192
195
  extinction coefficient ``k``.
193
196
 
197
+ Use ``material.n(wavelength)``, ``material.k(wavelength)``,
198
+ ``material.relative_permittivity(wavelength)``, and
199
+ ``material.absorption_coefficient(wavelength)`` for common derived properties.
200
+ See the documentation's physical-conventions page for the vacuum-wavelength,
201
+ ``n + i k``, GDD, interpolation, and extrapolation conventions.
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
+
194
232
  Validity ranges
195
233
  ~~~~~~~~~~~~~~~
196
234
 
@@ -235,6 +273,55 @@ These methods accept scalar or array wavelengths and return unit-aware values.
235
273
  ``dτ_g/dω`` (typically expressed in fs²); use
236
274
  ``compute_group_delay_wavelength_slope`` for ``dτ_g/dλ``.
237
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
+
238
325
  Plotting
239
326
  --------
240
327
 
@@ -319,6 +406,18 @@ The command-line interface provides the canonical catalog workflow:
319
406
  After installation, the equivalent console command is ``pyoptik
320
407
  download-all --data-root ./refractiveindex-data``.
321
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
+
322
421
  For a beginner-friendly first-time setup, use:
323
422
 
324
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.4'
22
- __version_tuple__ = version_tuple = (3, 0, 4)
21
+ __version__ = version = '3.1.0'
22
+ __version_tuple__ = version_tuple = (3, 1, 0)
23
23
 
24
- __commit_id__ = commit_id = 'gcad6d74ee'
24
+ __commit_id__ = commit_id = 'g705a5ec86'
@@ -122,9 +122,15 @@ class MaterialPage:
122
122
  state = "available" if self.available else "missing"
123
123
  return f"MaterialPage(id={self.id.key!r}, name={self.name!r}, data={state!r})"
124
124
 
125
- def load(self):
125
+ def load(self, *, interpolation: str = "linear"):
126
126
  """Load this page as the appropriate PyOptik material object.
127
127
 
128
+ Parameters
129
+ ----------
130
+ interpolation : {"linear", "pchip"}, optional
131
+ Tabulated-data interpolation method. Formula materials ignore this
132
+ option.
133
+
128
134
  Returns
129
135
  -------
130
136
  SellmeierMaterial or TabulatedMaterial
@@ -139,15 +145,24 @@ class MaterialPage:
139
145
  """
140
146
  from PyOptik.material.sellmeier_class import SellmeierMaterial
141
147
  from PyOptik.material.tabulated_class import TabulatedMaterial
148
+ from PyOptik.material.dataset import parse_material
142
149
 
143
150
  if self.local_path is not None and self.local_path.exists():
144
- with self.local_path.open("r") as stream:
145
- document = yaml.safe_load(stream) or {}
146
- entries = document.get("DATA", [])
147
- if any("formula" in str(entry.get("type", "")) for entry in entries):
148
- return SellmeierMaterial(self.name, file_path=self.local_path)
149
- if any("tabulated" in str(entry.get("type", "")) for entry in entries):
150
- return TabulatedMaterial(self.name, file_path=self.local_path)
151
+ document = parse_material(self.local_path)
152
+ if document.formula_datasets:
153
+ material = SellmeierMaterial(self.name, file_path=self.local_path)
154
+ material.catalog_id = self.id.key
155
+ material.source_url = self.source_url
156
+ return material
157
+ if document.tabulated_datasets:
158
+ material = TabulatedMaterial(
159
+ self.name,
160
+ file_path=self.local_path,
161
+ interpolation=interpolation,
162
+ )
163
+ material.catalog_id = self.id.key
164
+ material.source_url = self.source_url
165
+ return material
151
166
  raise ValueError(f"No supported optical dataset found in {self.local_path}")
152
167
 
153
168
  raise FileNotFoundError(
@@ -1,3 +1,4 @@
1
1
  from .base_class import BaseMaterial
2
2
  from .sellmeier_class import SellmeierMaterial
3
3
  from .tabulated_class import TabulatedMaterial
4
+ from .dataset import FormulaDataset, MaterialDocument, MaterialMetadata, TabulatedDataset, parse_material
@@ -17,6 +17,113 @@ class BaseMaterial(object):
17
17
  unit handling, validity-range checks, and group-delay calculations.
18
18
  """
19
19
 
20
+ @property
21
+ def provenance(self) -> dict:
22
+ """Return source and validity metadata for this material.
23
+
24
+ Returns
25
+ -------
26
+ dict
27
+ Material name, source file, reference, conditions, validity range,
28
+ and catalog identity when the material was loaded from a catalog.
29
+ """
30
+ return {
31
+ "name": self.filename,
32
+ "file_path": str(getattr(self, "file_path", "")) or None,
33
+ "reference": getattr(self, "reference", None),
34
+ "conditions": getattr(self, "conditions", None),
35
+ "comments": getattr(self, "comments", None),
36
+ "wavelength_range": getattr(self, "wavelength_bound", None),
37
+ "catalog_id": getattr(self, "catalog_id", None),
38
+ "source_url": getattr(self, "source_url", None),
39
+ }
40
+
41
+ def refractive_index(self, wavelength: Length, **kwargs):
42
+ """Return the complex refractive index ``n + i k``.
43
+
44
+ Parameters
45
+ ----------
46
+ wavelength : Length
47
+ Vacuum wavelength.
48
+ **kwargs
49
+ Forwarded to :meth:`compute_refractive_index`.
50
+
51
+ Returns
52
+ -------
53
+ complex or numpy.ndarray
54
+ Complex refractive index.
55
+ """
56
+ return self.compute_refractive_index(wavelength, **kwargs)
57
+
58
+ def n(self, wavelength: Length, **kwargs):
59
+ """Return the real refractive index.
60
+
61
+ Parameters
62
+ ----------
63
+ wavelength : Length
64
+ Vacuum wavelength.
65
+ **kwargs
66
+ Forwarded to :meth:`compute_refractive_index`.
67
+
68
+ Returns
69
+ -------
70
+ float or numpy.ndarray
71
+ Real part of the complex refractive index.
72
+ """
73
+ return self.compute_refractive_index(wavelength, **kwargs).real
74
+
75
+ def k(self, wavelength: Length, **kwargs):
76
+ """Return the extinction coefficient.
77
+
78
+ Parameters
79
+ ----------
80
+ wavelength : Length
81
+ Vacuum wavelength.
82
+ **kwargs
83
+ Forwarded to :meth:`compute_refractive_index`.
84
+
85
+ Returns
86
+ -------
87
+ float or numpy.ndarray
88
+ Imaginary part of the complex refractive index.
89
+ """
90
+ return self.compute_refractive_index(wavelength, **kwargs).imag
91
+
92
+ def relative_permittivity(self, wavelength: Length, **kwargs):
93
+ """Return relative complex permittivity, ``(n + i k)²``.
94
+
95
+ Parameters
96
+ ----------
97
+ wavelength : Length
98
+ Vacuum wavelength.
99
+ **kwargs
100
+ Forwarded to :meth:`compute_refractive_index`.
101
+
102
+ Returns
103
+ -------
104
+ complex or numpy.ndarray
105
+ Relative complex permittivity.
106
+ """
107
+ return self.compute_refractive_index(wavelength, **kwargs) ** 2
108
+
109
+ @validate_units
110
+ def absorption_coefficient(self, wavelength: Length, **kwargs) -> AnyUnit:
111
+ """Return the intensity absorption coefficient, ``α = 4πk / λ``.
112
+
113
+ Parameters
114
+ ----------
115
+ wavelength : Length
116
+ Vacuum wavelength.
117
+ **kwargs
118
+ Forwarded to :meth:`k`.
119
+
120
+ Returns
121
+ -------
122
+ AnyUnit
123
+ Absorption coefficient with inverse-length units.
124
+ """
125
+ return 4 * numpy.pi * self.k(wavelength, **kwargs) / wavelength
126
+
20
127
  def __eq__(self, other) -> bool:
21
128
  """Compare materials by concrete type and filename.
22
129
 
@@ -88,8 +195,13 @@ class BaseMaterial(object):
88
195
  raise ValueError("out_of_range must be 'warn', 'raise', or 'clip'.")
89
196
  if self.wavelength_bound is not None:
90
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)
91
203
 
92
- if numpy.any((wavelength < min_value) | (wavelength > max_value)):
204
+ if numpy.any(below | above):
93
205
  message = (
94
206
  f"Wavelength range goes from {wavelength.min().to_compact()} to {wavelength.max().to_compact()} "
95
207
  f"which is outside the allowable range of {min_value.to_compact()} to {max_value.to_compact()} µm. "