PyOptik 3.2.1__tar.gz → 3.3.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.2.1 → pyoptik-3.3.0}/CHANGELOG.md +26 -1
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PKG-INFO +115 -31
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/__init__.py +2 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/_version.py +3 -3
- pyoptik-3.3.0/PyOptik/discovery.py +277 -0
- pyoptik-3.3.0/PyOptik/material/__init__.py +28 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/material/base_class.py +18 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik.egg-info/PKG-INFO +115 -31
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik.egg-info/SOURCES.txt +16 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik.egg-info/scm_file_list.json +16 -0
- pyoptik-3.3.0/PyOptik.egg-info/scm_version.json +8 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/README.rst +114 -30
- pyoptik-3.3.0/docs/examples/catalog/plot_material_by_name.py +60 -0
- pyoptik-3.3.0/docs/source/_static/tutorials/antireflection_coating.png +0 -0
- pyoptik-3.3.0/docs/source/_static/tutorials/gold_optical_constants.png +0 -0
- pyoptik-3.3.0/docs/source/_static/tutorials/silica_dispersion.png +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/code.rst +12 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/examples.rst +7 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/getting_started.rst +25 -8
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/index.rst +12 -3
- pyoptik-3.3.0/docs/source/materials_and_catalog.rst +177 -0
- pyoptik-3.3.0/docs/source/tutorials/antireflection_coating.rst +60 -0
- pyoptik-3.3.0/docs/source/tutorials/gold_optical_constants.rst +62 -0
- pyoptik-3.3.0/docs/source/tutorials/silica_dispersion.rst +61 -0
- pyoptik-3.3.0/docs/source/tutorials.rst +12 -0
- pyoptik-3.3.0/docs/tutorials/antireflection_coating.ipynb +53 -0
- pyoptik-3.3.0/docs/tutorials/antireflection_coating.py +31 -0
- pyoptik-3.3.0/docs/tutorials/gold_optical_constants.ipynb +53 -0
- pyoptik-3.3.0/docs/tutorials/gold_optical_constants.py +25 -0
- pyoptik-3.3.0/docs/tutorials/silica_dispersion.ipynb +53 -0
- pyoptik-3.3.0/docs/tutorials/silica_dispersion.py +32 -0
- pyoptik-3.3.0/tests/test_discovery.py +212 -0
- pyoptik-3.2.1/PyOptik/material/__init__.py +0 -4
- pyoptik-3.2.1/PyOptik.egg-info/scm_version.json +0 -8
- pyoptik-3.2.1/docs/source/materials_and_catalog.rst +0 -64
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.coveragerc +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.flake8 +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.github/dependabot.yml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.github/workflows/deploy_PyPi.yml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.github/workflows/deploy_anaconda.yml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.github/workflows/deploy_coverage.yml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.github/workflows/deploy_documentation.yml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.github/workflows/quality.yml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.github/workflows/tests.yml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.gitignore +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/.pre-commit-config.yaml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/CONTRIBUTING.md +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/LICENSE +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/Makefile +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/__main__.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/catalog.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/directories.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/material/dataset.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/material/sellmeier_class.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/material/tabulated_class.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/material_type.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/thin_film.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/tui.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik/utils.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik.egg-info/dependency_links.txt +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik.egg-info/entry_points.txt +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik.egg-info/requires.txt +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/PyOptik.egg-info/top_level.txt +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/conda.recipe/meta.yaml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/Makefile +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/README.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/catalog/README.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/catalog/plot_catalog_search.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/custom_materials/README.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/custom_materials/plot_measured_material.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/group_properties/README.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/group_properties/plot_group_properties.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/interfaces/README.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/interfaces/plot_fresnel_angles.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/sellmeier/README.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/sellmeier/plot_bk7.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/sellmeier/plot_compare_glasses.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/sellmeier/plot_silica.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/sellmeier/plot_water.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/tabulated/README.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/tabulated/plot_polyethylene.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/tabulated/plot_silicon_nk.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/tabulated/plot_silver.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/thin_films/README.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/thin_films/plot_antireflection_coating.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/examples/thin_films/plot_bragg_mirror.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/images/example_bk7.png +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/images/logo.svg +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/make.bat +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/_static/default.css +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/_static/favicon.png +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/_static/favicon.svg +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/_static/logo-dark.svg +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/_static/logo.svg +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/_static/thumbnail.png +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/catalog_browser.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/conf.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/conventions.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/custom_materials.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/references.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/sg_execution_times.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/thin_films.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/docs/source/user_guide.rst +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/pyproject.toml +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/pytest.ini +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/setup.cfg +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/__init__.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/conftest.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_base_material_decorator.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_catalog.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_datasets_and_custom_materials.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_docstrings.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_main_cli.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_material_models.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_thin_film.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_tui.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_utils_extra.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tests/test_validation_and_logging.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tools/next_release_version.py +0 -0
- {pyoptik-3.2.1 → pyoptik-3.3.0}/tools/release_tag.py +0 -0
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.3.0] - 2026-10-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `material(name)` and `load_material(name)` for common-name and chemical-formula
|
|
15
|
+
lookup, with documented defaults for silica, gold, silver, water, and N-BK7.
|
|
16
|
+
- Explicit source overrides, canonical-ID lookup, and `use_default=False` for
|
|
17
|
+
workflows that require an explicit choice among competing datasets.
|
|
18
|
+
- `find_materials()` and `AmbiguousMaterialError.candidates` for discovery with
|
|
19
|
+
dataset descriptions and provenance; missing defaults never select substitutes.
|
|
20
|
+
- `nk()` as a short alias for complex refractive-index evaluation.
|
|
21
|
+
- A gallery example covering lookup, defaults, source overrides, and provenance.
|
|
22
|
+
- Three focused tutorials with plots, scripts, downloadable notebooks, and
|
|
23
|
+
Google Colab links: silica dispersion, gold optical constants, and coatings.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- The README leads with a useful plot and runnable example, with status badges
|
|
28
|
+
further down the page.
|
|
29
|
+
- Getting-started documentation and material-data tutorials now introduce
|
|
30
|
+
common-name lookup alongside the canonical catalog API.
|
|
31
|
+
- The existing `PyOptik.material` package remains importable and is callable as
|
|
32
|
+
a convenience loader, preserving existing class and submodule imports.
|
|
33
|
+
|
|
10
34
|
## [3.2.0] - 2026-09-16
|
|
11
35
|
|
|
12
36
|
### Added
|
|
@@ -79,6 +103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
79
103
|
plotting helpers.
|
|
80
104
|
- Retired logo assets.
|
|
81
105
|
|
|
82
|
-
[Unreleased]: https://github.com/MartinPdeS/PyOptik/compare/v3.
|
|
106
|
+
[Unreleased]: https://github.com/MartinPdeS/PyOptik/compare/v3.3.0...HEAD
|
|
107
|
+
[3.3.0]: https://github.com/MartinPdeS/PyOptik/compare/v3.2.1...v3.3.0
|
|
83
108
|
[3.2.0]: https://github.com/MartinPdeS/PyOptik/compare/v3.1.0...v3.2.0
|
|
84
109
|
[3.1.0]: https://github.com/MartinPdeS/PyOptik/compare/v3.0.5...v3.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyOptik
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.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
|
|
@@ -61,40 +61,97 @@ Dynamic: license-file
|
|
|
61
61
|
|
|
62
62
|
|logo|
|
|
63
63
|
|
|
64
|
-
.. list-table::
|
|
65
|
-
:widths: 35 65
|
|
66
|
-
:header-rows: 1
|
|
67
|
-
|
|
68
|
-
* - Badge
|
|
69
|
-
- Status
|
|
70
|
-
* - Python versions
|
|
71
|
-
- |python|
|
|
72
|
-
* - Documentation
|
|
73
|
-
- |docs|
|
|
74
|
-
* - Continuous integration
|
|
75
|
-
- |ci/cd|
|
|
76
|
-
* - Test coverage
|
|
77
|
-
- |coverage|
|
|
78
|
-
* - PyPI package
|
|
79
|
-
- |PyPi|
|
|
80
|
-
* - PyPI downloads
|
|
81
|
-
- |PyPi_download|
|
|
82
|
-
* - Anaconda package
|
|
83
|
-
- |anaconda|
|
|
84
|
-
* - Anaconda downloads
|
|
85
|
-
- |anaconda_download|
|
|
86
|
-
|
|
87
64
|
PyOptik
|
|
88
65
|
=======
|
|
89
66
|
|
|
90
|
-
**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
67
|
+
**Get wavelength-dependent optical constants and calculate dispersion or
|
|
68
|
+
coating reflectance in Python.** PyOptik combines the RefractiveIndex.INFO
|
|
69
|
+
material catalog with unit-aware calculations for optical materials,
|
|
70
|
+
interfaces, and coherent thin-film stacks.
|
|
71
|
+
|
|
72
|
+
.. image:: https://raw.githubusercontent.com/MartinPdeS/PyOptik/master/docs/source/_static/tutorials/antireflection_coating.png
|
|
73
|
+
:alt: Coated glass has a reflection minimum at 550 nm compared with uncoated glass.
|
|
74
|
+
:width: 700
|
|
75
|
+
|
|
76
|
+
Make your first plot
|
|
77
|
+
--------------------
|
|
78
|
+
|
|
79
|
+
Install PyOptik:
|
|
80
|
+
|
|
81
|
+
.. code-block:: bash
|
|
82
|
+
|
|
83
|
+
python -m pip install PyOptik
|
|
84
|
+
|
|
85
|
+
Compare bare glass with an ideal quarter-wave coating. This example needs
|
|
86
|
+
no material database download:
|
|
87
|
+
|
|
88
|
+
.. code-block:: python
|
|
89
|
+
|
|
90
|
+
import matplotlib.pyplot as plt
|
|
91
|
+
import numpy as np
|
|
92
|
+
from TypedUnit import ureg
|
|
93
|
+
from PyOptik import ThinFilmLayer, thin_film_stack
|
|
94
|
+
|
|
95
|
+
wavelengths = np.linspace(350, 900, 600) * ureg.nanometer
|
|
96
|
+
coating_index = np.sqrt(1.52)
|
|
97
|
+
coating = ThinFilmLayer(coating_index, 550 * ureg.nanometer / (4 * coating_index))
|
|
98
|
+
bare = thin_film_stack(wavelengths, [], substrate_index=1.52)
|
|
99
|
+
coated = thin_film_stack(wavelengths, [coating], substrate_index=1.52)
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
plt.plot(wavelengths.magnitude, 100 * bare.reflectance, label="Bare glass")
|
|
102
|
+
plt.plot(wavelengths.magnitude, 100 * coated.reflectance, label="Coated glass")
|
|
103
|
+
plt.xlabel("Vacuum wavelength [nm]")
|
|
104
|
+
plt.ylabel("Reflectance [%]")
|
|
105
|
+
plt.legend()
|
|
106
|
+
plt.show()
|
|
107
|
+
|
|
108
|
+
The ideal coating suppresses reflection at 550 nm. The
|
|
109
|
+
`full coating tutorial <https://martinpdes.github.io/PyOptik/docs/latest/tutorials/antireflection_coating.html>`_
|
|
110
|
+
explains the physics and assumptions.
|
|
111
|
+
|
|
112
|
+
Load a material by name
|
|
113
|
+
-----------------------
|
|
114
|
+
|
|
115
|
+
Use a familiar name or formula. Common names load documented datasets;
|
|
116
|
+
choose another source whenever your experiment needs it. The first lookup
|
|
117
|
+
downloads the material snapshot; later lookups use the local cache:
|
|
118
|
+
|
|
119
|
+
.. code-block:: python
|
|
120
|
+
|
|
121
|
+
from PyOptik import material
|
|
122
|
+
from TypedUnit import ureg
|
|
123
|
+
|
|
124
|
+
bk7 = material("N-BK7")
|
|
125
|
+
silica = material("SiO2")
|
|
126
|
+
gold = material("Au")
|
|
127
|
+
|
|
128
|
+
print(bk7.n(532 * ureg.nm))
|
|
129
|
+
print(gold.nk(633 * ureg.nm))
|
|
130
|
+
print(gold.catalog_id) # main/Au/Johnson
|
|
131
|
+
bk7.plot()
|
|
132
|
+
|
|
133
|
+
Defaults are Malitson for silica, Johnson and Christy for gold and silver,
|
|
134
|
+
Hale and Querry for water, and SCHOTT N-BK7 for ``BK7``/``N-BK7``. Inspect the
|
|
135
|
+
resolved source through ``catalog_id`` and ``provenance``. Override a default
|
|
136
|
+
with ``material("Au", source="Rakic-LD")``, or inspect all candidates with
|
|
137
|
+
``find_materials("gold")``. Use ``use_default=False`` to require a source
|
|
138
|
+
when several datasets match. Other ambiguous names list the candidates
|
|
139
|
+
instead of guessing. See the
|
|
140
|
+
`default-source table <https://martinpdes.github.io/PyOptik/docs/latest/materials_and_catalog.html#find-a-material-by-name>`_
|
|
141
|
+
for canonical IDs; use ``material("main/Au/Johnson")`` to pin a dataset.
|
|
142
|
+
|
|
143
|
+
Learn through a useful result
|
|
144
|
+
-----------------------------
|
|
145
|
+
|
|
146
|
+
* `Calculate silica group index and dispersion in Python <https://martinpdes.github.io/PyOptik/docs/latest/tutorials/silica_dispersion.html>`_
|
|
147
|
+
— compare phase and group index, then calculate GDD through 1 mm of glass.
|
|
148
|
+
* `Plot gold's refractive index and extinction coefficient <https://martinpdes.github.io/PyOptik/docs/latest/tutorials/gold_optical_constants.html>`_
|
|
149
|
+
— load measured optical constants and plot n and k.
|
|
150
|
+
* `Design an antireflection coating in Python <https://martinpdes.github.io/PyOptik/docs/latest/tutorials/antireflection_coating.html>`_
|
|
151
|
+
— choose a layer thickness and compare coated and uncoated glass.
|
|
152
|
+
|
|
153
|
+
Each tutorial includes a plot, a downloadable script and notebook, and an
|
|
154
|
+
**Open in Colab** button.
|
|
98
155
|
|
|
99
156
|
Documentation
|
|
100
157
|
-------------
|
|
@@ -117,6 +174,7 @@ The full documentation is organized by task:
|
|
|
117
174
|
Features
|
|
118
175
|
--------
|
|
119
176
|
|
|
177
|
+
* Material lookup by common name with documented defaults and source overrides.
|
|
120
178
|
* Sellmeier and other dispersion-formula models.
|
|
121
179
|
* Tabulated complex refractive index data, ``n + i k``.
|
|
122
180
|
* Unit-aware wavelength calculations through ``TypedUnit`` and Pint.
|
|
@@ -488,6 +546,32 @@ Common issues
|
|
|
488
546
|
* If a material cannot be found, run ``pyoptik setup`` or call
|
|
489
547
|
``download_snapshot()`` before loading its canonical page.
|
|
490
548
|
|
|
549
|
+
Project status
|
|
550
|
+
--------------
|
|
551
|
+
|
|
552
|
+
.. list-table::
|
|
553
|
+
:widths: 35 65
|
|
554
|
+
:header-rows: 1
|
|
555
|
+
|
|
556
|
+
* - Badge
|
|
557
|
+
- Status
|
|
558
|
+
* - Python versions
|
|
559
|
+
- |python|
|
|
560
|
+
* - Documentation
|
|
561
|
+
- |docs|
|
|
562
|
+
* - Continuous integration
|
|
563
|
+
- |ci/cd|
|
|
564
|
+
* - Test coverage
|
|
565
|
+
- |coverage|
|
|
566
|
+
* - PyPI package
|
|
567
|
+
- |PyPi|
|
|
568
|
+
* - PyPI downloads
|
|
569
|
+
- |PyPi_download|
|
|
570
|
+
* - Anaconda package
|
|
571
|
+
- |anaconda|
|
|
572
|
+
* - Anaconda downloads
|
|
573
|
+
- |anaconda_download|
|
|
574
|
+
|
|
491
575
|
Development and testing
|
|
492
576
|
-----------------------
|
|
493
577
|
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (3,
|
|
21
|
+
__version__ = version = '3.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 3, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gefc1d294d'
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Human-readable material discovery backed by canonical catalog identities."""
|
|
2
|
+
|
|
3
|
+
from difflib import get_close_matches
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import unicodedata
|
|
6
|
+
|
|
7
|
+
from .catalog import MaterialCatalog, MaterialPage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Aliases identify a material family; documented defaults are applied only
|
|
11
|
+
# by load_material, never by discovery or explicit source selection.
|
|
12
|
+
_BOOK_ALIASES = {
|
|
13
|
+
"sio2": ("main", "SiO2"),
|
|
14
|
+
"silica": ("main", "SiO2"),
|
|
15
|
+
"fusedsilica": ("main", "SiO2"),
|
|
16
|
+
"au": ("main", "Au"),
|
|
17
|
+
"gold": ("main", "Au"),
|
|
18
|
+
"ag": ("main", "Ag"),
|
|
19
|
+
"silver": ("main", "Ag"),
|
|
20
|
+
"si": ("main", "Si"),
|
|
21
|
+
"silicon": ("main", "Si"),
|
|
22
|
+
"h2o": ("main", "H2O"),
|
|
23
|
+
"water": ("main", "H2O"),
|
|
24
|
+
}
|
|
25
|
+
_PAGE_ALIASES = {
|
|
26
|
+
"bk7": "specs/SCHOTT-optical/N-BK7",
|
|
27
|
+
"nbk7": "specs/SCHOTT-optical/N-BK7",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# These source choices are part of the convenience API, not a ranking of
|
|
32
|
+
# measurement quality. Keep canonical IDs explicit and document any changes.
|
|
33
|
+
_DEFAULT_SOURCES = {
|
|
34
|
+
("main", "SiO2"): "main/SiO2/Malitson",
|
|
35
|
+
("main", "Au"): "main/Au/Johnson",
|
|
36
|
+
("main", "Ag"): "main/Ag/Johnson",
|
|
37
|
+
("main", "H2O"): "main/H2O/Hale",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _normalize(value: str) -> str:
|
|
42
|
+
"""Normalize case, accents, spacing, and punctuation for exact matching."""
|
|
43
|
+
value = unicodedata.normalize("NFKD", value).casefold()
|
|
44
|
+
return "".join(character for character in value if character.isalnum())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _nonempty(value: str, label: str) -> str:
|
|
48
|
+
"""Validate a required non-empty name or source argument."""
|
|
49
|
+
if not isinstance(value, str):
|
|
50
|
+
raise TypeError(f"{label} must be a string.")
|
|
51
|
+
if not _normalize(value):
|
|
52
|
+
raise ValueError(f"{label} must not be empty.")
|
|
53
|
+
return value.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_catalog(catalog: MaterialCatalog | None, data_root: Path | str | None) -> MaterialCatalog:
|
|
57
|
+
"""Read a local index or obtain the snapshot on the first lookup."""
|
|
58
|
+
if catalog is not None:
|
|
59
|
+
if data_root is not None:
|
|
60
|
+
raise ValueError("Pass either catalog or data_root, not both.")
|
|
61
|
+
return catalog
|
|
62
|
+
from .directories import user_data_path
|
|
63
|
+
|
|
64
|
+
root = Path(data_root or (user_data_path / "rii")).expanduser()
|
|
65
|
+
catalog_file = root / "catalog-nk.yml"
|
|
66
|
+
if catalog_file.is_file():
|
|
67
|
+
return MaterialCatalog(catalog_file=catalog_file, data_root=root)
|
|
68
|
+
return MaterialCatalog.from_snapshot(data_root=root)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _candidates(name: str, catalog: MaterialCatalog) -> list[MaterialPage]:
|
|
72
|
+
"""Resolve exact identities and families before falling back to text search."""
|
|
73
|
+
if "/" in name:
|
|
74
|
+
return [catalog.get(name)]
|
|
75
|
+
normalized = _normalize(name)
|
|
76
|
+
if normalized in _PAGE_ALIASES:
|
|
77
|
+
return [catalog.get(_PAGE_ALIASES[normalized])]
|
|
78
|
+
if normalized in _BOOK_ALIASES:
|
|
79
|
+
shelf, book = _BOOK_ALIASES[normalized]
|
|
80
|
+
return catalog.pages(shelf=shelf, book=book)
|
|
81
|
+
pages = catalog.pages()
|
|
82
|
+
exact_books = [page for page in pages if _normalize(page.id.book) == normalized]
|
|
83
|
+
if exact_books:
|
|
84
|
+
return exact_books
|
|
85
|
+
exact_pages = [page for page in pages if _normalize(page.id.page) == normalized]
|
|
86
|
+
if exact_pages:
|
|
87
|
+
return exact_pages
|
|
88
|
+
return catalog.search(name)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _select_source(pages: list[MaterialPage], source: str) -> list[MaterialPage]:
|
|
92
|
+
"""Prefer an exact page ID to a partial source or description match."""
|
|
93
|
+
normalized = _normalize(source)
|
|
94
|
+
exact = [page for page in pages if normalized in (
|
|
95
|
+
_normalize(page.id.page), _normalize(page.id.key),
|
|
96
|
+
)]
|
|
97
|
+
if exact:
|
|
98
|
+
return exact
|
|
99
|
+
return [page for page in pages if any(
|
|
100
|
+
normalized in _normalize(value)
|
|
101
|
+
for value in (page.id.page, page.description or "")
|
|
102
|
+
)]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AmbiguousMaterialError(ValueError):
|
|
106
|
+
"""A name or source matches several scientific datasets.
|
|
107
|
+
|
|
108
|
+
Attributes
|
|
109
|
+
----------
|
|
110
|
+
candidates : list of MaterialPage
|
|
111
|
+
Matching pages in canonical order, including descriptions and provenance.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(self, name: str, candidates: list[MaterialPage]):
|
|
115
|
+
"""List candidate identities and show how to select a source explicitly."""
|
|
116
|
+
self.candidates = candidates
|
|
117
|
+
lines = [f"Multiple datasets found for {name!r}:"]
|
|
118
|
+
lines.extend(
|
|
119
|
+
f" {number}. {page.id.key}: {page.description or page.name}"
|
|
120
|
+
for number, page in enumerate(candidates, start=1)
|
|
121
|
+
)
|
|
122
|
+
first = candidates[0]
|
|
123
|
+
lines.extend([
|
|
124
|
+
"",
|
|
125
|
+
f"Select a source with material({name!r}, source={first.id.page!r}),",
|
|
126
|
+
f"or use a canonical ID: material({first.id.key!r}).",
|
|
127
|
+
"Use find_materials(name) to inspect candidates and provenance.",
|
|
128
|
+
])
|
|
129
|
+
super().__init__("\n".join(lines))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def find_materials(
|
|
133
|
+
name: str,
|
|
134
|
+
*,
|
|
135
|
+
source: str | None = None,
|
|
136
|
+
catalog: MaterialCatalog | None = None,
|
|
137
|
+
data_root: Path | str | None = None,
|
|
138
|
+
) -> list[MaterialPage]:
|
|
139
|
+
"""Find material datasets by common name, formula, or canonical identity.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
name : str
|
|
144
|
+
Common name (gold, fused silica, water), formula (Au, SiO2), glass
|
|
145
|
+
name (BK7, N-BK7), or canonical shelf/book/page ID. BK7 is an explicit
|
|
146
|
+
alias for SCHOTT N-BK7. Other aliases select families; find_materials always returns all sources.
|
|
147
|
+
source : str, optional
|
|
148
|
+
Page ID, canonical ID, or source-description text. Exact page IDs
|
|
149
|
+
take precedence over partial matches.
|
|
150
|
+
catalog : MaterialCatalog, optional
|
|
151
|
+
Existing catalog, including custom or offline catalogs.
|
|
152
|
+
data_root : pathlib.Path or str, optional
|
|
153
|
+
Snapshot location. Cannot be combined with catalog.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
list of MaterialPage
|
|
158
|
+
Candidates in canonical order, or an empty list if none match.
|
|
159
|
+
|
|
160
|
+
Notes
|
|
161
|
+
-----
|
|
162
|
+
Names and source descriptions ignore case, accents, spaces, and punctuation
|
|
163
|
+
for exact matching. General text search uses the catalog's case-insensitive
|
|
164
|
+
substring search. The first lookup downloads the snapshot if no local
|
|
165
|
+
index exists. An existing index is read locally without a network request.
|
|
166
|
+
"""
|
|
167
|
+
name = _nonempty(name, "name")
|
|
168
|
+
if source is not None:
|
|
169
|
+
source = _nonempty(source, "source")
|
|
170
|
+
resolved_catalog = _get_catalog(catalog, data_root)
|
|
171
|
+
try:
|
|
172
|
+
pages = _candidates(name, resolved_catalog)
|
|
173
|
+
except KeyError:
|
|
174
|
+
return []
|
|
175
|
+
return _select_source(pages, source) if source is not None else pages
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def load_material(
|
|
179
|
+
name: str,
|
|
180
|
+
*,
|
|
181
|
+
source: str | None = None,
|
|
182
|
+
catalog: MaterialCatalog | None = None,
|
|
183
|
+
data_root: Path | str | None = None,
|
|
184
|
+
interpolation: str = "linear",
|
|
185
|
+
use_default: bool = True,
|
|
186
|
+
):
|
|
187
|
+
"""Load a material by name with documented defaults and source overrides.
|
|
188
|
+
|
|
189
|
+
This is also available as ``from PyOptik import material; material(...)``.
|
|
190
|
+
Common names use documented canonical defaults unless source is supplied
|
|
191
|
+
or use_default is False. Other ambiguous queries raise an error listing
|
|
192
|
+
sources and canonical IDs. Ordering and cache availability never select
|
|
193
|
+
a dataset.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
name : str
|
|
198
|
+
Common name, chemical formula, glass name, or canonical page ID.
|
|
199
|
+
source : str, optional
|
|
200
|
+
Exact page ID or source-description text. Use the exact ID when
|
|
201
|
+
a source publishes several datasets or measurement conditions.
|
|
202
|
+
catalog : MaterialCatalog, optional
|
|
203
|
+
Existing catalog for custom data or offline operation.
|
|
204
|
+
data_root : pathlib.Path or str, optional
|
|
205
|
+
Snapshot directory. Cannot be combined with catalog.
|
|
206
|
+
interpolation : {"linear", "pchip"}, optional
|
|
207
|
+
Interpolation for tabulated data; ignored for formula models.
|
|
208
|
+
use_default : bool, optional
|
|
209
|
+
Use the documented source for a known family alias (default True).
|
|
210
|
+
Set False to require a source whenever several datasets match.
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
SellmeierMaterial or TabulatedMaterial
|
|
215
|
+
Loaded model, retaining canonical ID and scientific provenance.
|
|
216
|
+
|
|
217
|
+
Raises
|
|
218
|
+
------
|
|
219
|
+
AmbiguousMaterialError
|
|
220
|
+
More than one dataset matches and no documented default applies, or
|
|
221
|
+
use_default is False. Inspect candidates or use find_materials.
|
|
222
|
+
ValueError
|
|
223
|
+
Empty name or source, or conflicting catalog/data_root arguments.
|
|
224
|
+
TypeError
|
|
225
|
+
Name or source is not a string, or use_default is not a bool.
|
|
226
|
+
KeyError
|
|
227
|
+
No material or source matches; includes spelling suggestions when possible.
|
|
228
|
+
FileNotFoundError
|
|
229
|
+
The chosen page has no local data. Run pyoptik setup to repair the cache.
|
|
230
|
+
|
|
231
|
+
Notes
|
|
232
|
+
-----
|
|
233
|
+
Defaults are main/SiO2/Malitson for silica, main/Au/Johnson for gold,
|
|
234
|
+
main/Ag/Johnson for silver, and main/H2O/Hale for water. BK7 and N-BK7
|
|
235
|
+
refer explicitly to specs/SCHOTT-optical/N-BK7. Loaded models expose
|
|
236
|
+
catalog_id and provenance. A missing default never falls back to another
|
|
237
|
+
dataset. See the materials-and-catalog guide for the full source table.
|
|
238
|
+
|
|
239
|
+
Examples
|
|
240
|
+
--------
|
|
241
|
+
>>> from PyOptik import material
|
|
242
|
+
>>> gold = material("gold", source="Johnson") # doctest: +SKIP
|
|
243
|
+
>>> gold.catalog_id # doctest: +SKIP
|
|
244
|
+
'main/Au/Johnson'
|
|
245
|
+
"""
|
|
246
|
+
name = _nonempty(name, "name")
|
|
247
|
+
if source is not None:
|
|
248
|
+
source = _nonempty(source, "source")
|
|
249
|
+
if not isinstance(use_default, bool):
|
|
250
|
+
raise TypeError("use_default must be a bool.")
|
|
251
|
+
resolved_catalog = _get_catalog(catalog, data_root)
|
|
252
|
+
default = _DEFAULT_SOURCES.get(_BOOK_ALIASES.get(_normalize(name)))
|
|
253
|
+
if source is None and use_default and default is not None:
|
|
254
|
+
try:
|
|
255
|
+
selected = resolved_catalog.get(default)
|
|
256
|
+
except KeyError as error:
|
|
257
|
+
raise KeyError(
|
|
258
|
+
f"Documented default {default!r} for {name!r} is absent from this catalog. "
|
|
259
|
+
"Use find_materials(name) and select an explicit source."
|
|
260
|
+
) from error
|
|
261
|
+
return selected.load(interpolation=interpolation)
|
|
262
|
+
pages = find_materials(name, catalog=resolved_catalog)
|
|
263
|
+
if not pages:
|
|
264
|
+
known_names = sorted(set(_BOOK_ALIASES) | set(_PAGE_ALIASES) |
|
|
265
|
+
{page.id.book for page in resolved_catalog.pages()})
|
|
266
|
+
suggestions = get_close_matches(_normalize(name), known_names, n=3, cutoff=0.6)
|
|
267
|
+
hint = f" Did you mean {', '.join(repr(item) for item in suggestions)}?" if suggestions else ""
|
|
268
|
+
raise KeyError(f"No material found for {name!r}.{hint} Use find_materials(name) to search the catalog.")
|
|
269
|
+
if source is not None:
|
|
270
|
+
selected = _select_source(pages, source)
|
|
271
|
+
if not selected:
|
|
272
|
+
available = ", ".join(page.id.key for page in pages)
|
|
273
|
+
raise KeyError(f"No source {source!r} found for {name!r}. Available datasets: {available}")
|
|
274
|
+
pages = selected
|
|
275
|
+
if len(pages) > 1:
|
|
276
|
+
raise AmbiguousMaterialError(name, pages)
|
|
277
|
+
return pages[0].load(interpolation=interpolation)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .base_class import BaseMaterial
|
|
2
|
+
from .sellmeier_class import SellmeierMaterial
|
|
3
|
+
from .tabulated_class import TabulatedMaterial
|
|
4
|
+
from .dataset import FormulaDataset, MaterialDocument, MaterialMetadata, TabulatedDataset, parse_material
|
|
5
|
+
|
|
6
|
+
# Preserve the existing material package and dotted imports while making the
|
|
7
|
+
# beginner-facing `from PyOptik import material` shortcut callable.
|
|
8
|
+
import sys as _sys
|
|
9
|
+
from types import ModuleType as _ModuleType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _MaterialModule(_ModuleType):
|
|
13
|
+
"""The material package with a convenience loader as its call interface."""
|
|
14
|
+
|
|
15
|
+
def __call__(
|
|
16
|
+
self, name, *, source=None, catalog=None, data_root=None, interpolation="linear",
|
|
17
|
+
use_default=True,
|
|
18
|
+
):
|
|
19
|
+
"""Load a material; see :func:`PyOptik.load_material` for selection rules."""
|
|
20
|
+
from PyOptik.discovery import load_material
|
|
21
|
+
|
|
22
|
+
return load_material(
|
|
23
|
+
name, source=source, catalog=catalog, data_root=data_root,
|
|
24
|
+
interpolation=interpolation, use_default=use_default,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_sys.modules[__name__].__class__ = _MaterialModule
|
|
@@ -56,6 +56,24 @@ class BaseMaterial(object):
|
|
|
56
56
|
"""
|
|
57
57
|
return self.compute_refractive_index(wavelength, **kwargs)
|
|
58
58
|
|
|
59
|
+
def nk(self, wavelength: Length, **kwargs):
|
|
60
|
+
"""Return the complex refractive index ``n + i k``.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
wavelength : Length
|
|
65
|
+
Vacuum wavelength, as a scalar or array with units.
|
|
66
|
+
**kwargs
|
|
67
|
+
Forwarded to :meth:`compute_refractive_index`, including
|
|
68
|
+
``out_of_range``.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
complex or numpy.ndarray
|
|
73
|
+
Complex refractive index. Alias of :meth:`refractive_index`.
|
|
74
|
+
"""
|
|
75
|
+
return self.compute_refractive_index(wavelength, **kwargs)
|
|
76
|
+
|
|
59
77
|
def n(self, wavelength: Length, **kwargs):
|
|
60
78
|
"""Return the real refractive index.
|
|
61
79
|
|