PyOptik 3.0.0__tar.gz → 3.0.5__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.0 → pyoptik-3.0.5}/.gitignore +1 -0
- pyoptik-3.0.5/CHANGELOG.md +47 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PKG-INFO +18 -3
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/_version.py +3 -3
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/catalog.py +210 -8
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/directories.py +1 -1
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/material/base_class.py +161 -3
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/material/sellmeier_class.py +77 -29
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/material/tabulated_class.py +98 -25
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik.egg-info/PKG-INFO +18 -3
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik.egg-info/SOURCES.txt +6 -1
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik.egg-info/requires.txt +0 -1
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik.egg-info/scm_file_list.json +6 -1
- pyoptik-3.0.5/PyOptik.egg-info/scm_version.json +8 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/README.rst +17 -1
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/group_properties/plot_group_properties.py +1 -3
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/sellmeier/plot_compare_glasses.py +1 -3
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/tabulated/plot_silicon_nk.py +1 -3
- pyoptik-3.0.5/docs/images/logo.svg +1 -0
- pyoptik-3.0.5/docs/source/_static/favicon.png +0 -0
- pyoptik-3.0.5/docs/source/_static/logo.svg +1 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/source/conf.py +3 -3
- pyoptik-3.0.5/docs/source/conventions.rst +49 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/source/index.rst +1 -0
- pyoptik-3.0.5/docs/source/sg_execution_times.rst +58 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/meta.yaml +0 -1
- {pyoptik-3.0.0 → pyoptik-3.0.5}/pyproject.toml +0 -1
- {pyoptik-3.0.0 → pyoptik-3.0.5}/tests/test_catalog.py +12 -0
- pyoptik-3.0.5/tests/test_material_models.py +138 -0
- pyoptik-3.0.0/PyOptik.egg-info/scm_version.json +0 -8
- pyoptik-3.0.0/docs/images/logo.png +0 -0
- pyoptik-3.0.0/tests/test_material_models.py +0 -64
- {pyoptik-3.0.0 → pyoptik-3.0.5}/.coveragerc +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/.flake8 +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/.github/dependabot.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/.github/workflows/deploy_PyPi.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/.github/workflows/deploy_anaconda.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/.github/workflows/deploy_coverage.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/.github/workflows/deploy_documentation.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/.github/workflows/tests.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/.pre-commit-config.yaml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/LICENSE +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/Makefile +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/__init__.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/__main__.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/material/__init__.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/material_type.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik/utils.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik.egg-info/dependency_links.txt +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik.egg-info/entry_points.txt +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/PyOptik.egg-info/top_level.txt +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/Makefile +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/README.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/group_properties/README.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/sellmeier/README.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/sellmeier/plot_bk7.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/sellmeier/plot_silica.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/sellmeier/plot_water.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/tabulated/README.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/tabulated/plot_polyethylene.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/examples/tabulated/plot_silver.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/images/example_bk7.png +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/make.bat +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/source/_static/default.css +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/source/_static/thumbnail.png +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/source/code.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/source/examples.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/docs/source/references.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/pytest.ini +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/setup.cfg +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/tests/__init__.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/tests/conftest.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/tests/test_base_material_decorator.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/tests/test_docstrings.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/tests/test_main_cli.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/tests/test_utils_extra.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.5}/tests/test_validation_and_logging.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Support for all nine RefractiveIndex.INFO dispersion formula types.
|
|
13
|
+
- `MaterialCatalog.search()` with hierarchy, source, reference, and local
|
|
14
|
+
availability filters.
|
|
15
|
+
- `MaterialPage.provenance()` for serializable material-source records.
|
|
16
|
+
- `MaterialCatalog.verify_integrity()` for SHA-256 verification of cached
|
|
17
|
+
material data.
|
|
18
|
+
- `compute_group_delay_wavelength_slope()` for the explicit wavelength-space
|
|
19
|
+
derivative, `dτ_g/dλ`.
|
|
20
|
+
- Convenience APIs for real index, extinction coefficient, relative
|
|
21
|
+
permittivity, and absorption coefficient.
|
|
22
|
+
- Split tabulated ``n`` and ``k`` YAML support and opt-in monotonic PCHIP
|
|
23
|
+
interpolation.
|
|
24
|
+
- A physical conventions and provenance documentation guide.
|
|
25
|
+
- Numerical regression, unit-equivalence, catalog-search, and cache-integrity
|
|
26
|
+
test coverage.
|
|
27
|
+
- A new PyOptik prism logo and documentation favicon.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `compute_group_delay_dispersion()` now returns conventional frequency-domain
|
|
32
|
+
GDD, `dτ_g/dω`, with time-squared units.
|
|
33
|
+
- Material plots use a consistent built-in Matplotlib layout and typography.
|
|
34
|
+
- Public catalog and group-delay APIs use expanded NumPy-style docstrings.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Formula type 6 now accumulates every gas-dispersion term instead of
|
|
39
|
+
overwriting earlier terms.
|
|
40
|
+
|
|
41
|
+
### Removed
|
|
42
|
+
|
|
43
|
+
- The `MPSPlots` runtime dependency and its use in examples and material
|
|
44
|
+
plotting helpers.
|
|
45
|
+
- Retired logo assets.
|
|
46
|
+
|
|
47
|
+
[Unreleased]: https://github.com/MartinPdeS/PyOptik/compare/v3.0.0...HEAD
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyOptik
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.5
|
|
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
|
|
@@ -40,7 +40,6 @@ Requires-Dist: numpy
|
|
|
40
40
|
Requires-Dist: matplotlib~=3.8
|
|
41
41
|
Requires-Dist: requests~=2.31
|
|
42
42
|
Requires-Dist: pyyaml~=6.0
|
|
43
|
-
Requires-Dist: MPSPlots
|
|
44
43
|
Requires-Dist: tabulate~=0.9
|
|
45
44
|
Requires-Dist: TypedUnit
|
|
46
45
|
Provides-Extra: testing
|
|
@@ -192,6 +191,12 @@ Tabulated materials interpolate complex optical constants:
|
|
|
192
191
|
The real part is the refractive index ``n`` and the imaginary part is the
|
|
193
192
|
extinction coefficient ``k``.
|
|
194
193
|
|
|
194
|
+
Use ``material.n(wavelength)``, ``material.k(wavelength)``,
|
|
195
|
+
``material.relative_permittivity(wavelength)``, and
|
|
196
|
+
``material.absorption_coefficient(wavelength)`` for common derived properties.
|
|
197
|
+
See the documentation's physical-conventions page for the vacuum-wavelength,
|
|
198
|
+
``n + i k``, GDD, interpolation, and extrapolation conventions.
|
|
199
|
+
|
|
195
200
|
Validity ranges
|
|
196
201
|
~~~~~~~~~~~~~~~
|
|
197
202
|
|
|
@@ -228,7 +233,13 @@ Every material model provides group-related quantities:
|
|
|
228
233
|
)
|
|
229
234
|
group_delay_dispersion = silica.compute_group_delay_dispersion(wavelength)
|
|
230
235
|
|
|
236
|
+
# The wavelength-space slope is available separately.
|
|
237
|
+
group_delay_wavelength_slope = silica.compute_group_delay_wavelength_slope(wavelength)
|
|
238
|
+
|
|
231
239
|
These methods accept scalar or array wavelengths and return unit-aware values.
|
|
240
|
+
``compute_group_delay_dispersion`` is the conventional frequency-domain GDD,
|
|
241
|
+
``dτ_g/dω`` (typically expressed in fs²); use
|
|
242
|
+
``compute_group_delay_wavelength_slope`` for ``dτ_g/dλ``.
|
|
232
243
|
|
|
233
244
|
Plotting
|
|
234
245
|
--------
|
|
@@ -296,6 +307,10 @@ Access a page by its canonical identifier and load its material model:
|
|
|
296
307
|
page = catalog.get("specs/SCHOTT-optical/N-BK7")
|
|
297
308
|
bk7 = page.load()
|
|
298
309
|
|
|
310
|
+
# Search by ID, descriptive name, or source URL; retain only cached pages.
|
|
311
|
+
for page in catalog.search("BK7", shelf="specs", available=True):
|
|
312
|
+
print(page.provenance())
|
|
313
|
+
|
|
299
314
|
Material data is cached in a user data directory. Set
|
|
300
315
|
``PYOPTIK_DATA_DIR`` to choose a different location.
|
|
301
316
|
|
|
@@ -388,7 +403,7 @@ material page for its original scientific or manufacturer reference.
|
|
|
388
403
|
:alt: Supported Python versions
|
|
389
404
|
:target: https://pypi.org/project/pyoptik/
|
|
390
405
|
|
|
391
|
-
.. |logo| image:: https://github.com/MartinPdeS/PyOptik/raw/master/docs/images/logo.
|
|
406
|
+
.. |logo| image:: https://github.com/MartinPdeS/PyOptik/raw/master/docs/images/logo.svg
|
|
392
407
|
:alt: PyOptik logo
|
|
393
408
|
|
|
394
409
|
.. |docs| image:: https://github.com/martinpdes/pyoptik/actions/workflows/deploy_documentation.yml/badge.svg
|
|
@@ -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, 0,
|
|
21
|
+
__version__ = version = '3.0.5'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 0, 5)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g3091905a0'
|
|
@@ -67,6 +67,41 @@ class MaterialPage:
|
|
|
67
67
|
metadata: Optional[dict] = None
|
|
68
68
|
data_root: Optional[Path] = None
|
|
69
69
|
|
|
70
|
+
@property
|
|
71
|
+
def available(self) -> bool:
|
|
72
|
+
"""Whether this page's data file is available in the local cache."""
|
|
73
|
+
return self.local_path is not None and self.local_path.exists()
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def reference(self) -> Optional[str]:
|
|
77
|
+
"""Return the local source reference, when the page has been cached."""
|
|
78
|
+
if not self.available:
|
|
79
|
+
return None
|
|
80
|
+
try:
|
|
81
|
+
with self.local_path.open("r") as stream:
|
|
82
|
+
reference = (yaml.safe_load(stream) or {}).get("REFERENCES")
|
|
83
|
+
return str(reference) if reference is not None else None
|
|
84
|
+
except (OSError, yaml.YAMLError):
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def provenance(self) -> dict:
|
|
88
|
+
"""Return the canonical identity and source information for this page.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
dict
|
|
93
|
+
Serializable identity, cache-path, availability, and source fields.
|
|
94
|
+
"""
|
|
95
|
+
return {
|
|
96
|
+
"id": self.id.key,
|
|
97
|
+
"name": self.name,
|
|
98
|
+
"description": self.description,
|
|
99
|
+
"reference": self.reference,
|
|
100
|
+
"source_url": self.source_url,
|
|
101
|
+
"local_path": str(self.local_path) if self.local_path else None,
|
|
102
|
+
"available": self.available,
|
|
103
|
+
}
|
|
104
|
+
|
|
70
105
|
@property
|
|
71
106
|
def local_path(self) -> Optional[Path]:
|
|
72
107
|
"""Return the local cached path, if a data path is known."""
|
|
@@ -84,11 +119,30 @@ class MaterialPage:
|
|
|
84
119
|
The representation intentionally omits YAML metadata and source URLs,
|
|
85
120
|
which may be large or distracting when inspecting a catalog.
|
|
86
121
|
"""
|
|
87
|
-
state = "available" if self.
|
|
122
|
+
state = "available" if self.available else "missing"
|
|
88
123
|
return f"MaterialPage(id={self.id.key!r}, name={self.name!r}, data={state!r})"
|
|
89
124
|
|
|
90
|
-
def load(self):
|
|
91
|
-
"""Load this page as the appropriate PyOptik material object.
|
|
125
|
+
def load(self, *, interpolation: str = "linear"):
|
|
126
|
+
"""Load this page as the appropriate PyOptik material object.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
interpolation : {"linear", "pchip"}, optional
|
|
131
|
+
Tabulated-data interpolation method. Formula materials ignore this
|
|
132
|
+
option.
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
SellmeierMaterial or TabulatedMaterial
|
|
137
|
+
Material model selected from the local YAML data type.
|
|
138
|
+
|
|
139
|
+
Raises
|
|
140
|
+
------
|
|
141
|
+
FileNotFoundError
|
|
142
|
+
If the page has not been downloaded locally.
|
|
143
|
+
ValueError
|
|
144
|
+
If the file contains no supported optical dataset.
|
|
145
|
+
"""
|
|
92
146
|
from PyOptik.material.sellmeier_class import SellmeierMaterial
|
|
93
147
|
from PyOptik.material.tabulated_class import TabulatedMaterial
|
|
94
148
|
|
|
@@ -97,9 +151,19 @@ class MaterialPage:
|
|
|
97
151
|
document = yaml.safe_load(stream) or {}
|
|
98
152
|
entries = document.get("DATA", [])
|
|
99
153
|
if any("formula" in str(entry.get("type", "")) for entry in entries):
|
|
100
|
-
|
|
154
|
+
material = SellmeierMaterial(self.name, file_path=self.local_path)
|
|
155
|
+
material.catalog_id = self.id.key
|
|
156
|
+
material.source_url = self.source_url
|
|
157
|
+
return material
|
|
101
158
|
if any("tabulated" in str(entry.get("type", "")) for entry in entries):
|
|
102
|
-
|
|
159
|
+
material = TabulatedMaterial(
|
|
160
|
+
self.name,
|
|
161
|
+
file_path=self.local_path,
|
|
162
|
+
interpolation=interpolation,
|
|
163
|
+
)
|
|
164
|
+
material.catalog_id = self.id.key
|
|
165
|
+
material.source_url = self.source_url
|
|
166
|
+
return material
|
|
103
167
|
raise ValueError(f"No supported optical dataset found in {self.local_path}")
|
|
104
168
|
|
|
105
169
|
raise FileNotFoundError(
|
|
@@ -146,7 +210,20 @@ class MaterialCatalog:
|
|
|
146
210
|
|
|
147
211
|
@classmethod
|
|
148
212
|
def from_upstream(cls, data_root: Optional[Path | str] = None, url: str = UPSTREAM_CATALOG_URL):
|
|
149
|
-
"""Download and load the current upstream catalog index.
|
|
213
|
+
"""Download and load the current upstream catalog index.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
data_root : pathlib.Path or str, optional
|
|
218
|
+
Directory used for the catalog index and downloaded pages.
|
|
219
|
+
url : str, optional
|
|
220
|
+
Catalog YAML URL.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
MaterialCatalog
|
|
225
|
+
Catalog indexed from the downloaded upstream YAML file.
|
|
226
|
+
"""
|
|
150
227
|
root = Path(data_root or (user_data_path / "rii")).expanduser()
|
|
151
228
|
catalog_file = root / "catalog-nk.yml"
|
|
152
229
|
download_yml_file(
|
|
@@ -288,6 +365,16 @@ class MaterialCatalog:
|
|
|
288
365
|
"mode": "snapshot",
|
|
289
366
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
290
367
|
}
|
|
368
|
+
for material_page in catalog.pages():
|
|
369
|
+
if material_page.local_path is None or not material_page.local_path.exists():
|
|
370
|
+
continue
|
|
371
|
+
manifest["pages"][material_page.id.key] = {
|
|
372
|
+
"source_url": material_page.source_url,
|
|
373
|
+
"local_path": str(material_page.local_path.relative_to(root)),
|
|
374
|
+
"status": "snapshot",
|
|
375
|
+
"sha256": catalog._sha256(material_page.local_path),
|
|
376
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
377
|
+
}
|
|
291
378
|
catalog._write_manifest(manifest)
|
|
292
379
|
logger.info("Extracted upstream snapshot with %s catalog pages", len(catalog.pages()))
|
|
293
380
|
return catalog
|
|
@@ -329,7 +416,18 @@ class MaterialCatalog:
|
|
|
329
416
|
logger.info("Loaded %s material pages from %s", len(self._pages), catalog_file)
|
|
330
417
|
|
|
331
418
|
def pages(self, shelf: Optional[str] = None, book: Optional[str] = None) -> list[MaterialPage]:
|
|
332
|
-
"""Return pages filtered by shelf and/or book.
|
|
419
|
+
"""Return pages filtered by shelf and/or book.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
shelf, book : str, optional
|
|
424
|
+
Canonical hierarchy filters.
|
|
425
|
+
|
|
426
|
+
Returns
|
|
427
|
+
-------
|
|
428
|
+
list of MaterialPage
|
|
429
|
+
Matching pages in canonical identifier order.
|
|
430
|
+
"""
|
|
333
431
|
return sorted(
|
|
334
432
|
(page for page in self._pages.values()
|
|
335
433
|
if (shelf is None or page.id.shelf == shelf)
|
|
@@ -337,6 +435,66 @@ class MaterialCatalog:
|
|
|
337
435
|
key=lambda page: page.id.key,
|
|
338
436
|
)
|
|
339
437
|
|
|
438
|
+
def search(
|
|
439
|
+
self,
|
|
440
|
+
query: Optional[str] = None,
|
|
441
|
+
*,
|
|
442
|
+
shelf: Optional[str] = None,
|
|
443
|
+
book: Optional[str] = None,
|
|
444
|
+
source: Optional[str] = None,
|
|
445
|
+
reference: Optional[str] = None,
|
|
446
|
+
available: Optional[bool] = None,
|
|
447
|
+
) -> list[MaterialPage]:
|
|
448
|
+
"""Search catalog pages by text and provenance-aware filters.
|
|
449
|
+
|
|
450
|
+
Text matching is case-insensitive and covers canonical IDs, page names,
|
|
451
|
+
catalog descriptions, and source URLs. ``reference`` filters the
|
|
452
|
+
local YAML ``REFERENCES`` field, so it requires cached pages. Set
|
|
453
|
+
``available=True`` to restrict results to local pages. Each result
|
|
454
|
+
exposes :meth:`MaterialPage.provenance` for a source record.
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
query : str, optional
|
|
459
|
+
Case-insensitive text query.
|
|
460
|
+
shelf, book : str, optional
|
|
461
|
+
Canonical hierarchy filters.
|
|
462
|
+
source : str, optional
|
|
463
|
+
Case-insensitive source-URL filter.
|
|
464
|
+
reference : str, optional
|
|
465
|
+
Case-insensitive local YAML reference filter.
|
|
466
|
+
available : bool, optional
|
|
467
|
+
Restrict results by cache availability.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
list of MaterialPage
|
|
472
|
+
Matching pages in canonical identifier order.
|
|
473
|
+
"""
|
|
474
|
+
terms = (query or "").casefold()
|
|
475
|
+
source_term = source.casefold() if source else None
|
|
476
|
+
reference_term = reference.casefold() if reference else None
|
|
477
|
+
matched = []
|
|
478
|
+
for material_page in self.pages(shelf=shelf, book=book):
|
|
479
|
+
haystack = " ".join(
|
|
480
|
+
value for value in (
|
|
481
|
+
material_page.id.key,
|
|
482
|
+
material_page.name,
|
|
483
|
+
material_page.description,
|
|
484
|
+
material_page.source_url,
|
|
485
|
+
) if value
|
|
486
|
+
).casefold()
|
|
487
|
+
if terms and terms not in haystack:
|
|
488
|
+
continue
|
|
489
|
+
if source_term and source_term not in (material_page.source_url or "").casefold():
|
|
490
|
+
continue
|
|
491
|
+
if reference_term and reference_term not in (material_page.reference or "").casefold():
|
|
492
|
+
continue
|
|
493
|
+
if available is not None and material_page.available is not available:
|
|
494
|
+
continue
|
|
495
|
+
matched.append(material_page)
|
|
496
|
+
return matched
|
|
497
|
+
|
|
340
498
|
def shelves(self) -> list[str]:
|
|
341
499
|
"""Return sorted upstream shelf identifiers.
|
|
342
500
|
|
|
@@ -399,8 +557,52 @@ class MaterialCatalog:
|
|
|
399
557
|
digest.update(block)
|
|
400
558
|
return digest.hexdigest()
|
|
401
559
|
|
|
560
|
+
def verify_integrity(self) -> dict[MaterialId, bool]:
|
|
561
|
+
"""Verify checksums recorded in the local download manifest.
|
|
562
|
+
|
|
563
|
+
Pages without a recorded checksum are omitted. The returned mapping is
|
|
564
|
+
keyed by canonical material identity, allowing callers to identify and
|
|
565
|
+
re-download only altered or incomplete cached files.
|
|
566
|
+
|
|
567
|
+
Returns
|
|
568
|
+
-------
|
|
569
|
+
dict of MaterialId to bool
|
|
570
|
+
Whether every manifest-recorded page matches its SHA-256 digest.
|
|
571
|
+
"""
|
|
572
|
+
checked = {}
|
|
573
|
+
for key, record in self._read_manifest().get("pages", {}).items():
|
|
574
|
+
expected = record.get("sha256")
|
|
575
|
+
relative_path = record.get("local_path")
|
|
576
|
+
if not expected or not relative_path:
|
|
577
|
+
continue
|
|
578
|
+
path = self.data_root / relative_path
|
|
579
|
+
parts = key.split("/")
|
|
580
|
+
if len(parts) < 3:
|
|
581
|
+
continue
|
|
582
|
+
identifier = MaterialId(parts[0], "/".join(parts[1:-1]), parts[-1])
|
|
583
|
+
checked[identifier] = path.is_file() and self._sha256(path) == expected
|
|
584
|
+
return checked
|
|
585
|
+
|
|
402
586
|
def get(self, identifier: MaterialId | str, book: Optional[str] = None, page: Optional[str] = None) -> MaterialPage:
|
|
403
|
-
"""Get a page by ``MaterialId``, canonical path, or components.
|
|
587
|
+
"""Get a page by ``MaterialId``, canonical path, or components.
|
|
588
|
+
|
|
589
|
+
Parameters
|
|
590
|
+
----------
|
|
591
|
+
identifier : MaterialId or str
|
|
592
|
+
Canonical identifier, or shelf when ``book`` and ``page`` are set.
|
|
593
|
+
book, page : str, optional
|
|
594
|
+
Remaining canonical hierarchy components.
|
|
595
|
+
|
|
596
|
+
Returns
|
|
597
|
+
-------
|
|
598
|
+
MaterialPage
|
|
599
|
+
The selected catalog page.
|
|
600
|
+
|
|
601
|
+
Raises
|
|
602
|
+
------
|
|
603
|
+
KeyError
|
|
604
|
+
If the identifier is unknown or incomplete.
|
|
605
|
+
"""
|
|
404
606
|
if isinstance(identifier, MaterialId):
|
|
405
607
|
key = identifier
|
|
406
608
|
elif book is not None and page is not None:
|
|
@@ -31,7 +31,7 @@ doc_path = project_path.joinpath('docs')
|
|
|
31
31
|
|
|
32
32
|
doc_css_path = doc_path.joinpath('source/_static/default.css')
|
|
33
33
|
|
|
34
|
-
logo_path = doc_path.joinpath('images/logo.
|
|
34
|
+
logo_path = doc_path.joinpath('images/logo.svg')
|
|
35
35
|
|
|
36
36
|
examples_path = root_path.joinpath('examples')
|
|
37
37
|
|
|
@@ -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
|
|
|
@@ -228,13 +335,15 @@ class BaseMaterial(object):
|
|
|
228
335
|
return length / vg
|
|
229
336
|
|
|
230
337
|
@validate_units
|
|
231
|
-
def
|
|
338
|
+
def compute_group_delay_wavelength_slope(
|
|
232
339
|
self,
|
|
233
340
|
wavelength: Length,
|
|
234
341
|
length: Length = 1 * ureg.meter,
|
|
235
342
|
delta: Length = 1 * ureg.nanometer,
|
|
236
343
|
) -> AnyUnit:
|
|
237
|
-
"""Calculate ``d
|
|
344
|
+
"""Calculate the wavelength slope ``dτ_g/dλ`` for a given length.
|
|
345
|
+
|
|
346
|
+
This is a wavelength-space derivative, not conventional GDD.
|
|
238
347
|
|
|
239
348
|
Parameters
|
|
240
349
|
----------
|
|
@@ -249,8 +358,57 @@ class BaseMaterial(object):
|
|
|
249
358
|
Returns
|
|
250
359
|
-------
|
|
251
360
|
AnyUnit
|
|
252
|
-
|
|
361
|
+
Wavelength derivative of group delay evaluated at ``wavelength``.
|
|
253
362
|
"""
|
|
254
363
|
gd_plus = self.compute_group_delay(wavelength + delta / 2, length)
|
|
255
364
|
gd_minus = self.compute_group_delay(wavelength - delta / 2, length)
|
|
256
365
|
return (gd_plus - gd_minus) / delta
|
|
366
|
+
|
|
367
|
+
@validate_units
|
|
368
|
+
def compute_group_delay_dispersion(
|
|
369
|
+
self,
|
|
370
|
+
wavelength: Length,
|
|
371
|
+
length: Length = 1 * ureg.meter,
|
|
372
|
+
delta_angular_frequency: AnyUnit = 2 * numpy.pi * 1e12 / ureg.second,
|
|
373
|
+
) -> AnyUnit:
|
|
374
|
+
"""Calculate conventional GDD, ``dτ_g/dω = d²β/dω² × length``.
|
|
375
|
+
|
|
376
|
+
The derivative is evaluated with a centred finite difference in
|
|
377
|
+
angular frequency and has time-squared units (commonly fs²). Use
|
|
378
|
+
:meth:`compute_group_delay_wavelength_slope` for ``dτ_g/dλ``.
|
|
379
|
+
|
|
380
|
+
Parameters
|
|
381
|
+
----------
|
|
382
|
+
wavelength : Length
|
|
383
|
+
Central vacuum wavelength.
|
|
384
|
+
length : Length, optional
|
|
385
|
+
Propagation length.
|
|
386
|
+
delta_angular_frequency : ureg.Quantity, optional
|
|
387
|
+
Positive centred angular-frequency increment. The default is
|
|
388
|
+
``2π × 1 THz``.
|
|
389
|
+
|
|
390
|
+
Returns
|
|
391
|
+
-------
|
|
392
|
+
AnyUnit
|
|
393
|
+
Conventional group-delay dispersion with time-squared units.
|
|
394
|
+
|
|
395
|
+
Raises
|
|
396
|
+
------
|
|
397
|
+
TypeError
|
|
398
|
+
If ``delta_angular_frequency`` does not carry units.
|
|
399
|
+
ValueError
|
|
400
|
+
If ``delta_angular_frequency`` is not positive.
|
|
401
|
+
"""
|
|
402
|
+
if not isinstance(delta_angular_frequency, ureg.Quantity):
|
|
403
|
+
raise TypeError("delta_angular_frequency must carry units.")
|
|
404
|
+
if delta_angular_frequency.magnitude <= 0:
|
|
405
|
+
raise ValueError("delta_angular_frequency must be positive.")
|
|
406
|
+
speed_of_light = 299792458 * ureg.meter / ureg.second
|
|
407
|
+
frequency_unit = 1 / ureg.second
|
|
408
|
+
omega = (2 * numpy.pi * speed_of_light / wavelength).to(frequency_unit)
|
|
409
|
+
delta = delta_angular_frequency.to(frequency_unit)
|
|
410
|
+
wavelength_plus = 2 * numpy.pi * speed_of_light / (omega + delta / 2)
|
|
411
|
+
wavelength_minus = 2 * numpy.pi * speed_of_light / (omega - delta / 2)
|
|
412
|
+
delay_plus = self.compute_group_delay(wavelength_plus, length)
|
|
413
|
+
delay_minus = self.compute_group_delay(wavelength_minus, length)
|
|
414
|
+
return (delay_plus - delay_minus) / delta
|