PyOptik 3.0.0__tar.gz → 3.0.4__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.4}/.gitignore +1 -0
- pyoptik-3.0.4/CHANGELOG.md +42 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PKG-INFO +12 -3
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/_version.py +3 -3
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/catalog.py +191 -5
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/directories.py +1 -1
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/material/base_class.py +54 -3
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/material/sellmeier_class.py +70 -27
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/material/tabulated_class.py +21 -9
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik.egg-info/PKG-INFO +12 -3
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik.egg-info/SOURCES.txt +5 -1
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik.egg-info/requires.txt +0 -1
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik.egg-info/scm_file_list.json +5 -1
- pyoptik-3.0.4/PyOptik.egg-info/scm_version.json +8 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/README.rst +11 -1
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/group_properties/plot_group_properties.py +1 -3
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/sellmeier/plot_compare_glasses.py +1 -3
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/tabulated/plot_silicon_nk.py +1 -3
- pyoptik-3.0.4/docs/images/logo.svg +1 -0
- pyoptik-3.0.4/docs/source/_static/favicon.png +0 -0
- pyoptik-3.0.4/docs/source/_static/logo.svg +1 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/source/conf.py +3 -3
- pyoptik-3.0.4/docs/source/sg_execution_times.rst +58 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/meta.yaml +0 -1
- {pyoptik-3.0.0 → pyoptik-3.0.4}/pyproject.toml +0 -1
- {pyoptik-3.0.0 → pyoptik-3.0.4}/tests/test_catalog.py +12 -0
- pyoptik-3.0.4/tests/test_material_models.py +114 -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.4}/.coveragerc +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/.flake8 +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/.github/dependabot.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/.github/workflows/deploy_PyPi.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/.github/workflows/deploy_anaconda.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/.github/workflows/deploy_coverage.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/.github/workflows/deploy_documentation.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/.github/workflows/tests.yml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/.pre-commit-config.yaml +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/LICENSE +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/Makefile +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/__init__.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/__main__.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/material/__init__.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/material_type.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik/utils.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik.egg-info/dependency_links.txt +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik.egg-info/entry_points.txt +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/PyOptik.egg-info/top_level.txt +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/Makefile +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/README.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/group_properties/README.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/sellmeier/README.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/sellmeier/plot_bk7.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/sellmeier/plot_silica.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/sellmeier/plot_water.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/tabulated/README.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/tabulated/plot_polyethylene.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/examples/tabulated/plot_silver.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/images/example_bk7.png +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/make.bat +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/source/_static/default.css +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/source/_static/thumbnail.png +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/source/code.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/source/examples.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/source/index.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/docs/source/references.rst +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/pytest.ini +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/setup.cfg +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/tests/__init__.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/tests/conftest.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/tests/test_base_material_decorator.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/tests/test_docstrings.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/tests/test_main_cli.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/tests/test_utils_extra.py +0 -0
- {pyoptik-3.0.0 → pyoptik-3.0.4}/tests/test_validation_and_logging.py +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
- Numerical regression, unit-equivalence, catalog-search, and cache-integrity
|
|
21
|
+
test coverage.
|
|
22
|
+
- A new PyOptik prism logo and documentation favicon.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `compute_group_delay_dispersion()` now returns conventional frequency-domain
|
|
27
|
+
GDD, `dτ_g/dω`, with time-squared units.
|
|
28
|
+
- Material plots use a consistent built-in Matplotlib layout and typography.
|
|
29
|
+
- Public catalog and group-delay APIs use expanded NumPy-style docstrings.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Formula type 6 now accumulates every gas-dispersion term instead of
|
|
34
|
+
overwriting earlier terms.
|
|
35
|
+
|
|
36
|
+
### Removed
|
|
37
|
+
|
|
38
|
+
- The `MPSPlots` runtime dependency and its use in examples and material
|
|
39
|
+
plotting helpers.
|
|
40
|
+
- Retired logo assets.
|
|
41
|
+
|
|
42
|
+
[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.4
|
|
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
|
|
@@ -228,7 +227,13 @@ Every material model provides group-related quantities:
|
|
|
228
227
|
)
|
|
229
228
|
group_delay_dispersion = silica.compute_group_delay_dispersion(wavelength)
|
|
230
229
|
|
|
230
|
+
# The wavelength-space slope is available separately.
|
|
231
|
+
group_delay_wavelength_slope = silica.compute_group_delay_wavelength_slope(wavelength)
|
|
232
|
+
|
|
231
233
|
These methods accept scalar or array wavelengths and return unit-aware values.
|
|
234
|
+
``compute_group_delay_dispersion`` is the conventional frequency-domain GDD,
|
|
235
|
+
``dτ_g/dω`` (typically expressed in fs²); use
|
|
236
|
+
``compute_group_delay_wavelength_slope`` for ``dτ_g/dλ``.
|
|
232
237
|
|
|
233
238
|
Plotting
|
|
234
239
|
--------
|
|
@@ -296,6 +301,10 @@ Access a page by its canonical identifier and load its material model:
|
|
|
296
301
|
page = catalog.get("specs/SCHOTT-optical/N-BK7")
|
|
297
302
|
bk7 = page.load()
|
|
298
303
|
|
|
304
|
+
# Search by ID, descriptive name, or source URL; retain only cached pages.
|
|
305
|
+
for page in catalog.search("BK7", shelf="specs", available=True):
|
|
306
|
+
print(page.provenance())
|
|
307
|
+
|
|
299
308
|
Material data is cached in a user data directory. Set
|
|
300
309
|
``PYOPTIK_DATA_DIR`` to choose a different location.
|
|
301
310
|
|
|
@@ -388,7 +397,7 @@ material page for its original scientific or manufacturer reference.
|
|
|
388
397
|
:alt: Supported Python versions
|
|
389
398
|
:target: https://pypi.org/project/pyoptik/
|
|
390
399
|
|
|
391
|
-
.. |logo| image:: https://github.com/MartinPdeS/PyOptik/raw/master/docs/images/logo.
|
|
400
|
+
.. |logo| image:: https://github.com/MartinPdeS/PyOptik/raw/master/docs/images/logo.svg
|
|
392
401
|
:alt: PyOptik logo
|
|
393
402
|
|
|
394
403
|
.. |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.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 0, 4)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gcad6d74ee'
|
|
@@ -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,24 @@ 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
125
|
def load(self):
|
|
91
|
-
"""Load this page as the appropriate PyOptik material object.
|
|
126
|
+
"""Load this page as the appropriate PyOptik material object.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
SellmeierMaterial or TabulatedMaterial
|
|
131
|
+
Material model selected from the local YAML data type.
|
|
132
|
+
|
|
133
|
+
Raises
|
|
134
|
+
------
|
|
135
|
+
FileNotFoundError
|
|
136
|
+
If the page has not been downloaded locally.
|
|
137
|
+
ValueError
|
|
138
|
+
If the file contains no supported optical dataset.
|
|
139
|
+
"""
|
|
92
140
|
from PyOptik.material.sellmeier_class import SellmeierMaterial
|
|
93
141
|
from PyOptik.material.tabulated_class import TabulatedMaterial
|
|
94
142
|
|
|
@@ -146,7 +194,20 @@ class MaterialCatalog:
|
|
|
146
194
|
|
|
147
195
|
@classmethod
|
|
148
196
|
def from_upstream(cls, data_root: Optional[Path | str] = None, url: str = UPSTREAM_CATALOG_URL):
|
|
149
|
-
"""Download and load the current upstream catalog index.
|
|
197
|
+
"""Download and load the current upstream catalog index.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
data_root : pathlib.Path or str, optional
|
|
202
|
+
Directory used for the catalog index and downloaded pages.
|
|
203
|
+
url : str, optional
|
|
204
|
+
Catalog YAML URL.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
MaterialCatalog
|
|
209
|
+
Catalog indexed from the downloaded upstream YAML file.
|
|
210
|
+
"""
|
|
150
211
|
root = Path(data_root or (user_data_path / "rii")).expanduser()
|
|
151
212
|
catalog_file = root / "catalog-nk.yml"
|
|
152
213
|
download_yml_file(
|
|
@@ -288,6 +349,16 @@ class MaterialCatalog:
|
|
|
288
349
|
"mode": "snapshot",
|
|
289
350
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
290
351
|
}
|
|
352
|
+
for material_page in catalog.pages():
|
|
353
|
+
if material_page.local_path is None or not material_page.local_path.exists():
|
|
354
|
+
continue
|
|
355
|
+
manifest["pages"][material_page.id.key] = {
|
|
356
|
+
"source_url": material_page.source_url,
|
|
357
|
+
"local_path": str(material_page.local_path.relative_to(root)),
|
|
358
|
+
"status": "snapshot",
|
|
359
|
+
"sha256": catalog._sha256(material_page.local_path),
|
|
360
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
361
|
+
}
|
|
291
362
|
catalog._write_manifest(manifest)
|
|
292
363
|
logger.info("Extracted upstream snapshot with %s catalog pages", len(catalog.pages()))
|
|
293
364
|
return catalog
|
|
@@ -329,7 +400,18 @@ class MaterialCatalog:
|
|
|
329
400
|
logger.info("Loaded %s material pages from %s", len(self._pages), catalog_file)
|
|
330
401
|
|
|
331
402
|
def pages(self, shelf: Optional[str] = None, book: Optional[str] = None) -> list[MaterialPage]:
|
|
332
|
-
"""Return pages filtered by shelf and/or book.
|
|
403
|
+
"""Return pages filtered by shelf and/or book.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
shelf, book : str, optional
|
|
408
|
+
Canonical hierarchy filters.
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
list of MaterialPage
|
|
413
|
+
Matching pages in canonical identifier order.
|
|
414
|
+
"""
|
|
333
415
|
return sorted(
|
|
334
416
|
(page for page in self._pages.values()
|
|
335
417
|
if (shelf is None or page.id.shelf == shelf)
|
|
@@ -337,6 +419,66 @@ class MaterialCatalog:
|
|
|
337
419
|
key=lambda page: page.id.key,
|
|
338
420
|
)
|
|
339
421
|
|
|
422
|
+
def search(
|
|
423
|
+
self,
|
|
424
|
+
query: Optional[str] = None,
|
|
425
|
+
*,
|
|
426
|
+
shelf: Optional[str] = None,
|
|
427
|
+
book: Optional[str] = None,
|
|
428
|
+
source: Optional[str] = None,
|
|
429
|
+
reference: Optional[str] = None,
|
|
430
|
+
available: Optional[bool] = None,
|
|
431
|
+
) -> list[MaterialPage]:
|
|
432
|
+
"""Search catalog pages by text and provenance-aware filters.
|
|
433
|
+
|
|
434
|
+
Text matching is case-insensitive and covers canonical IDs, page names,
|
|
435
|
+
catalog descriptions, and source URLs. ``reference`` filters the
|
|
436
|
+
local YAML ``REFERENCES`` field, so it requires cached pages. Set
|
|
437
|
+
``available=True`` to restrict results to local pages. Each result
|
|
438
|
+
exposes :meth:`MaterialPage.provenance` for a source record.
|
|
439
|
+
|
|
440
|
+
Parameters
|
|
441
|
+
----------
|
|
442
|
+
query : str, optional
|
|
443
|
+
Case-insensitive text query.
|
|
444
|
+
shelf, book : str, optional
|
|
445
|
+
Canonical hierarchy filters.
|
|
446
|
+
source : str, optional
|
|
447
|
+
Case-insensitive source-URL filter.
|
|
448
|
+
reference : str, optional
|
|
449
|
+
Case-insensitive local YAML reference filter.
|
|
450
|
+
available : bool, optional
|
|
451
|
+
Restrict results by cache availability.
|
|
452
|
+
|
|
453
|
+
Returns
|
|
454
|
+
-------
|
|
455
|
+
list of MaterialPage
|
|
456
|
+
Matching pages in canonical identifier order.
|
|
457
|
+
"""
|
|
458
|
+
terms = (query or "").casefold()
|
|
459
|
+
source_term = source.casefold() if source else None
|
|
460
|
+
reference_term = reference.casefold() if reference else None
|
|
461
|
+
matched = []
|
|
462
|
+
for material_page in self.pages(shelf=shelf, book=book):
|
|
463
|
+
haystack = " ".join(
|
|
464
|
+
value for value in (
|
|
465
|
+
material_page.id.key,
|
|
466
|
+
material_page.name,
|
|
467
|
+
material_page.description,
|
|
468
|
+
material_page.source_url,
|
|
469
|
+
) if value
|
|
470
|
+
).casefold()
|
|
471
|
+
if terms and terms not in haystack:
|
|
472
|
+
continue
|
|
473
|
+
if source_term and source_term not in (material_page.source_url or "").casefold():
|
|
474
|
+
continue
|
|
475
|
+
if reference_term and reference_term not in (material_page.reference or "").casefold():
|
|
476
|
+
continue
|
|
477
|
+
if available is not None and material_page.available is not available:
|
|
478
|
+
continue
|
|
479
|
+
matched.append(material_page)
|
|
480
|
+
return matched
|
|
481
|
+
|
|
340
482
|
def shelves(self) -> list[str]:
|
|
341
483
|
"""Return sorted upstream shelf identifiers.
|
|
342
484
|
|
|
@@ -399,8 +541,52 @@ class MaterialCatalog:
|
|
|
399
541
|
digest.update(block)
|
|
400
542
|
return digest.hexdigest()
|
|
401
543
|
|
|
544
|
+
def verify_integrity(self) -> dict[MaterialId, bool]:
|
|
545
|
+
"""Verify checksums recorded in the local download manifest.
|
|
546
|
+
|
|
547
|
+
Pages without a recorded checksum are omitted. The returned mapping is
|
|
548
|
+
keyed by canonical material identity, allowing callers to identify and
|
|
549
|
+
re-download only altered or incomplete cached files.
|
|
550
|
+
|
|
551
|
+
Returns
|
|
552
|
+
-------
|
|
553
|
+
dict of MaterialId to bool
|
|
554
|
+
Whether every manifest-recorded page matches its SHA-256 digest.
|
|
555
|
+
"""
|
|
556
|
+
checked = {}
|
|
557
|
+
for key, record in self._read_manifest().get("pages", {}).items():
|
|
558
|
+
expected = record.get("sha256")
|
|
559
|
+
relative_path = record.get("local_path")
|
|
560
|
+
if not expected or not relative_path:
|
|
561
|
+
continue
|
|
562
|
+
path = self.data_root / relative_path
|
|
563
|
+
parts = key.split("/")
|
|
564
|
+
if len(parts) < 3:
|
|
565
|
+
continue
|
|
566
|
+
identifier = MaterialId(parts[0], "/".join(parts[1:-1]), parts[-1])
|
|
567
|
+
checked[identifier] = path.is_file() and self._sha256(path) == expected
|
|
568
|
+
return checked
|
|
569
|
+
|
|
402
570
|
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.
|
|
571
|
+
"""Get a page by ``MaterialId``, canonical path, or components.
|
|
572
|
+
|
|
573
|
+
Parameters
|
|
574
|
+
----------
|
|
575
|
+
identifier : MaterialId or str
|
|
576
|
+
Canonical identifier, or shelf when ``book`` and ``page`` are set.
|
|
577
|
+
book, page : str, optional
|
|
578
|
+
Remaining canonical hierarchy components.
|
|
579
|
+
|
|
580
|
+
Returns
|
|
581
|
+
-------
|
|
582
|
+
MaterialPage
|
|
583
|
+
The selected catalog page.
|
|
584
|
+
|
|
585
|
+
Raises
|
|
586
|
+
------
|
|
587
|
+
KeyError
|
|
588
|
+
If the identifier is unknown or incomplete.
|
|
589
|
+
"""
|
|
404
590
|
if isinstance(identifier, MaterialId):
|
|
405
591
|
key = identifier
|
|
406
592
|
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
|
|
|
@@ -228,13 +228,15 @@ class BaseMaterial(object):
|
|
|
228
228
|
return length / vg
|
|
229
229
|
|
|
230
230
|
@validate_units
|
|
231
|
-
def
|
|
231
|
+
def compute_group_delay_wavelength_slope(
|
|
232
232
|
self,
|
|
233
233
|
wavelength: Length,
|
|
234
234
|
length: Length = 1 * ureg.meter,
|
|
235
235
|
delta: Length = 1 * ureg.nanometer,
|
|
236
236
|
) -> AnyUnit:
|
|
237
|
-
"""Calculate ``d
|
|
237
|
+
"""Calculate the wavelength slope ``dτ_g/dλ`` for a given length.
|
|
238
|
+
|
|
239
|
+
This is a wavelength-space derivative, not conventional GDD.
|
|
238
240
|
|
|
239
241
|
Parameters
|
|
240
242
|
----------
|
|
@@ -249,8 +251,57 @@ class BaseMaterial(object):
|
|
|
249
251
|
Returns
|
|
250
252
|
-------
|
|
251
253
|
AnyUnit
|
|
252
|
-
|
|
254
|
+
Wavelength derivative of group delay evaluated at ``wavelength``.
|
|
253
255
|
"""
|
|
254
256
|
gd_plus = self.compute_group_delay(wavelength + delta / 2, length)
|
|
255
257
|
gd_minus = self.compute_group_delay(wavelength - delta / 2, length)
|
|
256
258
|
return (gd_plus - gd_minus) / delta
|
|
259
|
+
|
|
260
|
+
@validate_units
|
|
261
|
+
def compute_group_delay_dispersion(
|
|
262
|
+
self,
|
|
263
|
+
wavelength: Length,
|
|
264
|
+
length: Length = 1 * ureg.meter,
|
|
265
|
+
delta_angular_frequency: AnyUnit = 2 * numpy.pi * 1e12 / ureg.second,
|
|
266
|
+
) -> AnyUnit:
|
|
267
|
+
"""Calculate conventional GDD, ``dτ_g/dω = d²β/dω² × length``.
|
|
268
|
+
|
|
269
|
+
The derivative is evaluated with a centred finite difference in
|
|
270
|
+
angular frequency and has time-squared units (commonly fs²). Use
|
|
271
|
+
:meth:`compute_group_delay_wavelength_slope` for ``dτ_g/dλ``.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
wavelength : Length
|
|
276
|
+
Central vacuum wavelength.
|
|
277
|
+
length : Length, optional
|
|
278
|
+
Propagation length.
|
|
279
|
+
delta_angular_frequency : ureg.Quantity, optional
|
|
280
|
+
Positive centred angular-frequency increment. The default is
|
|
281
|
+
``2π × 1 THz``.
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
AnyUnit
|
|
286
|
+
Conventional group-delay dispersion with time-squared units.
|
|
287
|
+
|
|
288
|
+
Raises
|
|
289
|
+
------
|
|
290
|
+
TypeError
|
|
291
|
+
If ``delta_angular_frequency`` does not carry units.
|
|
292
|
+
ValueError
|
|
293
|
+
If ``delta_angular_frequency`` is not positive.
|
|
294
|
+
"""
|
|
295
|
+
if not isinstance(delta_angular_frequency, ureg.Quantity):
|
|
296
|
+
raise TypeError("delta_angular_frequency must carry units.")
|
|
297
|
+
if delta_angular_frequency.magnitude <= 0:
|
|
298
|
+
raise ValueError("delta_angular_frequency must be positive.")
|
|
299
|
+
speed_of_light = 299792458 * ureg.meter / ureg.second
|
|
300
|
+
frequency_unit = 1 / ureg.second
|
|
301
|
+
omega = (2 * numpy.pi * speed_of_light / wavelength).to(frequency_unit)
|
|
302
|
+
delta = delta_angular_frequency.to(frequency_unit)
|
|
303
|
+
wavelength_plus = 2 * numpy.pi * speed_of_light / (omega + delta / 2)
|
|
304
|
+
wavelength_minus = 2 * numpy.pi * speed_of_light / (omega - delta / 2)
|
|
305
|
+
delay_plus = self.compute_group_delay(wavelength_plus, length)
|
|
306
|
+
delay_minus = self.compute_group_delay(wavelength_minus, length)
|
|
307
|
+
return (delay_plus - delay_minus) / delta
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import yaml
|
|
5
5
|
import numpy
|
|
6
|
-
import itertools
|
|
7
6
|
import logging
|
|
8
|
-
from
|
|
7
|
+
from matplotlib import pyplot as plt
|
|
9
8
|
from TypedUnit import Length, RefractiveIndex, validate_units, ureg
|
|
10
9
|
|
|
11
10
|
from PyOptik.directories import material_paths
|
|
@@ -83,14 +82,11 @@ class SellmeierMaterial(BaseMaterial):
|
|
|
83
82
|
coefficients = list(map(float, data["coefficients"].split()))
|
|
84
83
|
except (KeyError, IndexError, AttributeError, TypeError, ValueError) as error:
|
|
85
84
|
raise ValueError(f"Invalid Sellmeier data in YAML file {file_path}") from error
|
|
86
|
-
if self.formula_type not in
|
|
85
|
+
if self.formula_type not in set(range(1, 10)):
|
|
87
86
|
raise ValueError(f"Unsupported formula type: {self.formula_type}")
|
|
88
87
|
if not coefficients or not numpy.all(numpy.isfinite(coefficients)):
|
|
89
88
|
raise ValueError(f"Sellmeier coefficients must be finite in {file_path}")
|
|
90
89
|
|
|
91
|
-
# Extract coefficients and ensure the list has exactly 7 coefficients by padding with zeros if necessary
|
|
92
|
-
if len(coefficients) < 7:
|
|
93
|
-
coefficients.extend([0.0] * (7 - len(coefficients)))
|
|
94
90
|
self.coefficients = numpy.array(coefficients)
|
|
95
91
|
|
|
96
92
|
# Extract wavelength range
|
|
@@ -141,47 +137,83 @@ class SellmeierMaterial(BaseMaterial):
|
|
|
141
137
|
if out_of_range == "clip":
|
|
142
138
|
wavelength = self._clip_wavelength(wavelength)
|
|
143
139
|
|
|
144
|
-
#
|
|
145
|
-
|
|
140
|
+
# Formula definitions follow the RefractiveIndex.INFO database
|
|
141
|
+
# documentation. Wavelengths are expressed in micrometres here.
|
|
142
|
+
wavelength_um = wavelength.to(ureg.micrometer).magnitude
|
|
143
|
+
coefficients = self.coefficients
|
|
144
|
+
padded = numpy.pad(coefficients, (0, 10))
|
|
146
145
|
|
|
147
146
|
match self.formula_type:
|
|
148
147
|
case 1: # Formula 1 computation (standard Sellmeier)
|
|
149
|
-
n_squared = 1.0
|
|
150
|
-
for
|
|
151
|
-
n_squared +=
|
|
152
|
-
|
|
148
|
+
n_squared = 1.0 + padded[0]
|
|
149
|
+
for B, C in zip(coefficients[1::2], coefficients[2::2]):
|
|
150
|
+
n_squared += B * wavelength_um**2 / (wavelength_um**2 - C**2)
|
|
153
151
|
n = numpy.sqrt(n_squared)
|
|
154
152
|
|
|
155
153
|
case 2: # Formula 2 computation (extended Sellmeier)
|
|
156
|
-
n_squared = 1 +
|
|
157
|
-
for
|
|
158
|
-
n_squared +=
|
|
154
|
+
n_squared = 1 + padded[0]
|
|
155
|
+
for B, C in zip(coefficients[1::2], coefficients[2::2]):
|
|
156
|
+
n_squared += B * wavelength_um**2 / (wavelength_um**2 - C)
|
|
157
|
+
n = numpy.sqrt(n_squared)
|
|
158
|
+
|
|
159
|
+
case 3: # Polynomial
|
|
160
|
+
n_squared = padded[0]
|
|
161
|
+
for B, exponent in zip(coefficients[1::2], coefficients[2::2]):
|
|
162
|
+
n_squared += B * wavelength_um**exponent
|
|
163
|
+
n = numpy.sqrt(n_squared)
|
|
164
|
+
|
|
165
|
+
case 4: # RefractiveIndex.INFO
|
|
166
|
+
n_squared = padded[0]
|
|
167
|
+
for index in range(1, min(8, len(coefficients)), 4):
|
|
168
|
+
B, exponent, C, power = padded[index:index + 4]
|
|
169
|
+
n_squared += B * wavelength_um**exponent / (wavelength_um**2 - C**power)
|
|
170
|
+
for B, exponent in zip(coefficients[9::2], coefficients[10::2]):
|
|
171
|
+
n_squared += B * wavelength_um**exponent
|
|
159
172
|
n = numpy.sqrt(n_squared)
|
|
160
173
|
|
|
161
174
|
case 5: # Formula 5 computation (extended Sellmeier)
|
|
162
|
-
n =
|
|
163
|
-
for
|
|
164
|
-
n += B *
|
|
175
|
+
n = padded[0]
|
|
176
|
+
for B, exponent in zip(coefficients[1::2], coefficients[2::2]):
|
|
177
|
+
n += B * wavelength_um**exponent
|
|
165
178
|
|
|
166
179
|
case 6:
|
|
167
|
-
n = 1 +
|
|
168
|
-
for
|
|
169
|
-
n
|
|
180
|
+
n = 1 + padded[0]
|
|
181
|
+
for B, C in zip(coefficients[1::2], coefficients[2::2]):
|
|
182
|
+
n += B / (C - wavelength_um**-2)
|
|
183
|
+
|
|
184
|
+
case 7: # Herzberger
|
|
185
|
+
n = padded[0] + padded[1] / (wavelength_um**2 - 0.028)
|
|
186
|
+
n += padded[2] / (wavelength_um**2 - 0.028)**2
|
|
187
|
+
for index, coefficient in enumerate(coefficients[3:], start=3):
|
|
188
|
+
n += coefficient * wavelength_um**(2 * (index - 2))
|
|
189
|
+
|
|
190
|
+
case 8: # Retro
|
|
191
|
+
temporary = padded[0] + padded[1] * wavelength_um**2 / (wavelength_um**2 - padded[2])
|
|
192
|
+
temporary += padded[3] * wavelength_um**2
|
|
193
|
+
n = numpy.sqrt((2 * temporary + 1) / (1 - temporary))
|
|
194
|
+
|
|
195
|
+
case 9: # Exotic
|
|
196
|
+
n = numpy.sqrt(
|
|
197
|
+
padded[0]
|
|
198
|
+
+ padded[1] / (wavelength_um**2 - padded[2])
|
|
199
|
+
+ padded[3] * (wavelength_um - padded[4])
|
|
200
|
+
/ ((wavelength_um - padded[4])**2 + padded[5])
|
|
201
|
+
)
|
|
170
202
|
|
|
171
203
|
case _:
|
|
172
204
|
raise ValueError(f"Unsupported formula type: {self.formula_type}")
|
|
173
205
|
|
|
174
206
|
return n[0] if return_as_scalar else n
|
|
175
207
|
|
|
176
|
-
|
|
177
|
-
def plot(self, axes, samples: int = 100) -> None:
|
|
208
|
+
def plot(self, axes=None, samples: int = 100) -> None:
|
|
178
209
|
"""
|
|
179
210
|
Plots the refractive index as a function of wavelength over a specified range.
|
|
180
211
|
|
|
181
212
|
Parameters
|
|
182
213
|
----------
|
|
183
|
-
axes : matplotlib.axes.Axes
|
|
184
|
-
Axes on which to draw the dispersion curve.
|
|
214
|
+
axes : matplotlib.axes.Axes, optional
|
|
215
|
+
Axes on which to draw the dispersion curve. A compact, styled
|
|
216
|
+
figure is created when omitted.
|
|
185
217
|
samples : int
|
|
186
218
|
The number of samples to use for the wavelength range.
|
|
187
219
|
|
|
@@ -195,6 +227,11 @@ class SellmeierMaterial(BaseMaterial):
|
|
|
195
227
|
ValueError
|
|
196
228
|
If the wavelength is not a 1D array or list of float values.
|
|
197
229
|
"""
|
|
230
|
+
if axes is None:
|
|
231
|
+
figure, axes = plt.subplots(figsize=(7, 4.5), layout="constrained")
|
|
232
|
+
else:
|
|
233
|
+
figure = axes.figure
|
|
234
|
+
figure.set_size_inches(7, 4.5, forward=True)
|
|
198
235
|
wavelength = numpy.linspace(
|
|
199
236
|
self.wavelength_bound[0].magnitude,
|
|
200
237
|
self.wavelength_bound[1].magnitude,
|
|
@@ -207,10 +244,16 @@ class SellmeierMaterial(BaseMaterial):
|
|
|
207
244
|
axes.set(
|
|
208
245
|
ylabel='Refractive Index',
|
|
209
246
|
xlabel=r'Wavelength [$\mu$m]',
|
|
210
|
-
title=f"Refractive
|
|
247
|
+
title=f"Refractive index: {self.filename}",
|
|
211
248
|
)
|
|
212
|
-
axes.
|
|
249
|
+
axes.set_title(axes.get_title(), fontsize=14, pad=12)
|
|
250
|
+
axes.set_xlabel(axes.get_xlabel(), fontsize=11)
|
|
251
|
+
axes.set_ylabel(axes.get_ylabel(), fontsize=11)
|
|
252
|
+
axes.tick_params(labelsize=10)
|
|
253
|
+
axes.grid(alpha=0.25, linewidth=0.7)
|
|
254
|
+
axes.plot(wavelength.to(ureg.micrometer).magnitude, refractive_index.real, linewidth=2, label='n')
|
|
213
255
|
axes.legend()
|
|
256
|
+
return None
|
|
214
257
|
|
|
215
258
|
def print(self) -> str:
|
|
216
259
|
"""
|