PyOptik 3.0.4__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.4 → pyoptik-3.0.5}/CHANGELOG.md +5 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PKG-INFO +7 -1
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/_version.py +3 -3
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/catalog.py +19 -3
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/material/base_class.py +107 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/material/sellmeier_class.py +7 -2
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/material/tabulated_class.py +77 -16
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik.egg-info/PKG-INFO +7 -1
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik.egg-info/SOURCES.txt +1 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik.egg-info/scm_file_list.json +1 -0
- pyoptik-3.0.5/PyOptik.egg-info/scm_version.json +8 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/README.rst +6 -0
- pyoptik-3.0.5/docs/source/conventions.rst +49 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/index.rst +1 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/tests/test_material_models.py +24 -0
- pyoptik-3.0.4/PyOptik.egg-info/scm_version.json +0 -8
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.coveragerc +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.flake8 +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.github/dependabot.yml +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.github/workflows/deploy_PyPi.yml +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.github/workflows/deploy_anaconda.yml +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.github/workflows/deploy_coverage.yml +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.github/workflows/deploy_documentation.yml +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.github/workflows/tests.yml +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.gitignore +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/.pre-commit-config.yaml +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/LICENSE +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/Makefile +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/__init__.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/__main__.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/directories.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/material/__init__.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/material_type.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik/utils.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik.egg-info/dependency_links.txt +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik.egg-info/entry_points.txt +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik.egg-info/requires.txt +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/PyOptik.egg-info/top_level.txt +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/Makefile +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/README.rst +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/group_properties/README.rst +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/group_properties/plot_group_properties.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/sellmeier/README.rst +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/sellmeier/plot_bk7.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/sellmeier/plot_compare_glasses.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/sellmeier/plot_silica.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/sellmeier/plot_water.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/tabulated/README.rst +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/tabulated/plot_polyethylene.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/tabulated/plot_silicon_nk.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/examples/tabulated/plot_silver.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/images/example_bk7.png +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/images/logo.svg +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/make.bat +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/_static/default.css +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/_static/favicon.png +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/_static/logo.svg +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/_static/thumbnail.png +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/code.rst +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/conf.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/examples.rst +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/references.rst +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/docs/source/sg_execution_times.rst +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/meta.yaml +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/pyproject.toml +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/pytest.ini +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/setup.cfg +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/tests/__init__.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/tests/conftest.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/tests/test_base_material_decorator.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/tests/test_catalog.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/tests/test_docstrings.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/tests/test_main_cli.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/tests/test_utils_extra.py +0 -0
- {pyoptik-3.0.4 → pyoptik-3.0.5}/tests/test_validation_and_logging.py +0 -0
|
@@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
17
|
material data.
|
|
18
18
|
- `compute_group_delay_wavelength_slope()` for the explicit wavelength-space
|
|
19
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.
|
|
20
25
|
- Numerical regression, unit-equivalence, catalog-search, and cache-integrity
|
|
21
26
|
test coverage.
|
|
22
27
|
- A new PyOptik prism logo and documentation favicon.
|
|
@@ -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
|
|
@@ -191,6 +191,12 @@ Tabulated materials interpolate complex optical constants:
|
|
|
191
191
|
The real part is the refractive index ``n`` and the imaginary part is the
|
|
192
192
|
extinction coefficient ``k``.
|
|
193
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
|
+
|
|
194
200
|
Validity ranges
|
|
195
201
|
~~~~~~~~~~~~~~~
|
|
196
202
|
|
|
@@ -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'
|
|
@@ -122,9 +122,15 @@ class MaterialPage:
|
|
|
122
122
|
state = "available" if self.available else "missing"
|
|
123
123
|
return f"MaterialPage(id={self.id.key!r}, name={self.name!r}, data={state!r})"
|
|
124
124
|
|
|
125
|
-
def load(self):
|
|
125
|
+
def load(self, *, interpolation: str = "linear"):
|
|
126
126
|
"""Load this page as the appropriate PyOptik material object.
|
|
127
127
|
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
interpolation : {"linear", "pchip"}, optional
|
|
131
|
+
Tabulated-data interpolation method. Formula materials ignore this
|
|
132
|
+
option.
|
|
133
|
+
|
|
128
134
|
Returns
|
|
129
135
|
-------
|
|
130
136
|
SellmeierMaterial or TabulatedMaterial
|
|
@@ -145,9 +151,19 @@ class MaterialPage:
|
|
|
145
151
|
document = yaml.safe_load(stream) or {}
|
|
146
152
|
entries = document.get("DATA", [])
|
|
147
153
|
if any("formula" in str(entry.get("type", "")) for entry in entries):
|
|
148
|
-
|
|
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
|
|
149
158
|
if any("tabulated" in str(entry.get("type", "")) for entry in entries):
|
|
150
|
-
|
|
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
|
|
151
167
|
raise ValueError(f"No supported optical dataset found in {self.local_path}")
|
|
152
168
|
|
|
153
169
|
raise FileNotFoundError(
|
|
@@ -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
|
|
|
@@ -50,6 +50,8 @@ class SellmeierMaterial(BaseMaterial):
|
|
|
50
50
|
self.coefficients = None
|
|
51
51
|
self.wavelength_bound = None
|
|
52
52
|
self.reference = None
|
|
53
|
+
self.conditions = None
|
|
54
|
+
self.comments = None
|
|
53
55
|
self.formula_type = None
|
|
54
56
|
|
|
55
57
|
self._load_coefficients()
|
|
@@ -101,14 +103,17 @@ class SellmeierMaterial(BaseMaterial):
|
|
|
101
103
|
else:
|
|
102
104
|
self.wavelength_bound = None
|
|
103
105
|
|
|
104
|
-
#
|
|
106
|
+
# Preserve upstream provenance and experimental conditions.
|
|
105
107
|
self.reference = parsed_yaml.get('REFERENCES', None)
|
|
108
|
+
self.conditions = parsed_yaml.get('CONDITIONS', {})
|
|
109
|
+
self.comments = parsed_yaml.get('COMMENTS', None)
|
|
106
110
|
logger.debug("Validated Sellmeier material '%s' with formula %s", self.filename, self.formula_type)
|
|
107
111
|
|
|
108
112
|
@validate_units
|
|
109
113
|
def compute_refractive_index(self, wavelength: Length | float, out_of_range: str = "warn") -> RefractiveIndex:
|
|
110
114
|
r"""
|
|
111
|
-
Computes the refractive index n(\u03bb) using the appropriate
|
|
115
|
+
Computes the refractive index n(\u03bb) using the appropriate
|
|
116
|
+
RefractiveIndex.INFO dispersion formula (types 1 through 9).
|
|
112
117
|
|
|
113
118
|
Parameters
|
|
114
119
|
----------
|
|
@@ -32,7 +32,7 @@ class TabulatedMaterial(BaseMaterial):
|
|
|
32
32
|
Reference information for the material data.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
def __init__(self, filename: str, file_path=None):
|
|
35
|
+
def __init__(self, filename: str, file_path=None, interpolation: str = "linear"):
|
|
36
36
|
"""
|
|
37
37
|
Initializes the TabulatedMaterial with a filename.
|
|
38
38
|
|
|
@@ -43,9 +43,15 @@ class TabulatedMaterial(BaseMaterial):
|
|
|
43
43
|
file_path : pathlib.Path, optional
|
|
44
44
|
Explicit YAML path for hierarchy-preserving catalog pages. When
|
|
45
45
|
omitted, the user material directory is searched.
|
|
46
|
+
interpolation : {"linear", "pchip"}, optional
|
|
47
|
+
Interpolation method. ``"linear"`` is the default;
|
|
48
|
+
``"pchip"`` provides monotonic piecewise-cubic interpolation.
|
|
46
49
|
"""
|
|
47
50
|
self.filename = filename
|
|
48
51
|
self.file_path = file_path
|
|
52
|
+
if interpolation not in {"linear", "pchip"}:
|
|
53
|
+
raise ValueError("interpolation must be 'linear' or 'pchip'.")
|
|
54
|
+
self.interpolation = interpolation
|
|
49
55
|
|
|
50
56
|
# Initialize attributes
|
|
51
57
|
self.wavelength_bound = None
|
|
@@ -53,6 +59,10 @@ class TabulatedMaterial(BaseMaterial):
|
|
|
53
59
|
self.n_values = None
|
|
54
60
|
self.k_values = None
|
|
55
61
|
self.reference = None
|
|
62
|
+
self.conditions = None
|
|
63
|
+
self.comments = None
|
|
64
|
+
self._n_wavelength = None
|
|
65
|
+
self._k_wavelength = None
|
|
56
66
|
|
|
57
67
|
# Load tabulated data from the YAML file
|
|
58
68
|
self._load_tabulated_data()
|
|
@@ -86,24 +96,41 @@ class TabulatedMaterial(BaseMaterial):
|
|
|
86
96
|
logger.debug("Loaded tabulated data from %s", file_path)
|
|
87
97
|
|
|
88
98
|
try:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
for entry in parsed_yaml['DATA']:
|
|
100
|
+
kind = str(entry.get('type', '')).lower().split()
|
|
101
|
+
if len(kind) != 2 or kind[0] != 'tabulated' or kind[1] not in {'n', 'k', 'nk'}:
|
|
102
|
+
continue
|
|
103
|
+
data = numpy.array(
|
|
104
|
+
[[float(value) for value in row.split()] for row in entry['data'].strip().splitlines()]
|
|
105
|
+
)
|
|
106
|
+
expected_columns = 3 if kind[1] == 'nk' else 2
|
|
107
|
+
if data.ndim != 2 or data.shape[1] != expected_columns or len(data) < 2:
|
|
108
|
+
raise ValueError(f"tabulated {kind[1]} data must contain at least two valid rows")
|
|
109
|
+
if not numpy.all(numpy.isfinite(data)) or numpy.any(numpy.diff(data[:, 0]) <= 0):
|
|
110
|
+
raise ValueError("tabulated wavelengths must be finite and strictly increasing")
|
|
111
|
+
wavelengths = data[:, 0] * ureg.micrometer
|
|
112
|
+
if kind[1] in {'n', 'nk'}:
|
|
113
|
+
self._n_wavelength = wavelengths
|
|
114
|
+
self.n_values = data[:, 1]
|
|
115
|
+
if kind[1] == 'k':
|
|
116
|
+
self._k_wavelength = wavelengths
|
|
117
|
+
self.k_values = data[:, 1]
|
|
118
|
+
elif kind[1] == 'nk':
|
|
119
|
+
self._k_wavelength = wavelengths
|
|
120
|
+
self.k_values = data[:, 2]
|
|
121
|
+
if self.n_values is None and self.k_values is None:
|
|
122
|
+
raise ValueError("no supported tabulated n, k, or nk dataset found")
|
|
123
|
+
self.wavelength = self._n_wavelength if self._n_wavelength is not None else self._k_wavelength
|
|
124
|
+
except (KeyError, IndexError, TypeError, ValueError) as error:
|
|
101
125
|
raise ValueError(f"Invalid or missing data in YAML file {file_path}")
|
|
102
126
|
|
|
103
|
-
|
|
127
|
+
ranges = [values for values in (self._n_wavelength, self._k_wavelength) if values is not None]
|
|
128
|
+
self.wavelength_bound = [min(values.min().magnitude for values in ranges), max(values.max().magnitude for values in ranges)] * ureg.micrometer
|
|
104
129
|
|
|
105
130
|
# Extract reference
|
|
106
131
|
self.reference = parsed_yaml.get('REFERENCES', None)
|
|
132
|
+
self.conditions = parsed_yaml.get('CONDITIONS', {})
|
|
133
|
+
self.comments = parsed_yaml.get('COMMENTS', None)
|
|
107
134
|
logger.debug("Validated tabulated material '%s' with %s points", self.filename, len(self.wavelength))
|
|
108
135
|
|
|
109
136
|
@validate_units
|
|
@@ -145,13 +172,47 @@ class TabulatedMaterial(BaseMaterial):
|
|
|
145
172
|
# behavior explicit for future range implementations.
|
|
146
173
|
pass
|
|
147
174
|
|
|
148
|
-
|
|
149
|
-
|
|
175
|
+
values = wavelength.to(ureg.meter).magnitude
|
|
176
|
+
n_interp = self._interpolate(values, self._n_wavelength, self.n_values, default=1.0)
|
|
177
|
+
k_interp = self._interpolate(values, self._k_wavelength, self.k_values, default=0.0)
|
|
150
178
|
|
|
151
179
|
index = n_interp + 1j * k_interp
|
|
152
180
|
|
|
153
181
|
return index[0] if return_as_scalar else index
|
|
154
182
|
|
|
183
|
+
def _interpolate(self, values, wavelengths, data, default: float):
|
|
184
|
+
"""Interpolate one optical-constant component with endpoint extrapolation."""
|
|
185
|
+
if wavelengths is None or data is None:
|
|
186
|
+
return numpy.full(values.shape, default, dtype=float)
|
|
187
|
+
x = wavelengths.to(ureg.meter).magnitude
|
|
188
|
+
if self.interpolation == "linear":
|
|
189
|
+
result = numpy.interp(values, x, data)
|
|
190
|
+
left = values < x[0]
|
|
191
|
+
right = values > x[-1]
|
|
192
|
+
result[left] = data[0] + (values[left] - x[0]) * (data[1] - data[0]) / (x[1] - x[0])
|
|
193
|
+
result[right] = data[-1] + (values[right] - x[-1]) * (data[-1] - data[-2]) / (x[-1] - x[-2])
|
|
194
|
+
return result
|
|
195
|
+
return self._pchip(values, x, data)
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _pchip(values, x, y):
|
|
199
|
+
"""Evaluate monotonic cubic Hermite interpolation with linear extrapolation."""
|
|
200
|
+
h = numpy.diff(x)
|
|
201
|
+
delta = numpy.diff(y) / h
|
|
202
|
+
slopes = numpy.empty_like(y, dtype=float)
|
|
203
|
+
slopes[0], slopes[-1] = delta[0], delta[-1]
|
|
204
|
+
for index in range(1, len(y) - 1):
|
|
205
|
+
if delta[index - 1] * delta[index] <= 0:
|
|
206
|
+
slopes[index] = 0.0
|
|
207
|
+
else:
|
|
208
|
+
w1, w2 = 2 * h[index] + h[index - 1], h[index] + 2 * h[index - 1]
|
|
209
|
+
slopes[index] = (w1 + w2) / (w1 / delta[index - 1] + w2 / delta[index])
|
|
210
|
+
intervals = numpy.clip(numpy.searchsorted(x, values, side='right') - 1, 0, len(x) - 2)
|
|
211
|
+
step = h[intervals]
|
|
212
|
+
t = (values - x[intervals]) / step
|
|
213
|
+
result = ((2*t**3 - 3*t**2 + 1) * y[intervals] + (t**3 - 2*t**2 + t) * step * slopes[intervals] + (-2*t**3 + 3*t**2) * y[intervals + 1] + (t**3 - t**2) * step * slopes[intervals + 1])
|
|
214
|
+
return result
|
|
215
|
+
|
|
155
216
|
def plot(self, axes=None, samples: int = 100) -> None:
|
|
156
217
|
"""
|
|
157
218
|
Plots the tabulated refractive index (n) and absorption (k) as a function of wavelength.
|
|
@@ -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
|
|
@@ -191,6 +191,12 @@ Tabulated materials interpolate complex optical constants:
|
|
|
191
191
|
The real part is the refractive index ``n`` and the imaginary part is the
|
|
192
192
|
extinction coefficient ``k``.
|
|
193
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
|
+
|
|
194
200
|
Validity ranges
|
|
195
201
|
~~~~~~~~~~~~~~~
|
|
196
202
|
|
|
@@ -132,6 +132,12 @@ Tabulated materials interpolate complex optical constants:
|
|
|
132
132
|
The real part is the refractive index ``n`` and the imaginary part is the
|
|
133
133
|
extinction coefficient ``k``.
|
|
134
134
|
|
|
135
|
+
Use ``material.n(wavelength)``, ``material.k(wavelength)``,
|
|
136
|
+
``material.relative_permittivity(wavelength)``, and
|
|
137
|
+
``material.absorption_coefficient(wavelength)`` for common derived properties.
|
|
138
|
+
See the documentation's physical-conventions page for the vacuum-wavelength,
|
|
139
|
+
``n + i k``, GDD, interpolation, and extrapolation conventions.
|
|
140
|
+
|
|
135
141
|
Validity ranges
|
|
136
142
|
~~~~~~~~~~~~~~~
|
|
137
143
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Physical conventions and data provenance
|
|
2
|
+
========================================
|
|
3
|
+
|
|
4
|
+
Wavelength and optical constants
|
|
5
|
+
--------------------------------
|
|
6
|
+
|
|
7
|
+
PyOptik accepts vacuum wavelength quantities. Always attach units—for example
|
|
8
|
+
``1550 * ureg.nanometer``—to avoid ambiguity. Bare numeric inputs are retained
|
|
9
|
+
for compatibility and interpreted as metres.
|
|
10
|
+
|
|
11
|
+
The complex refractive index uses the convention ``N = n + i k``: ``n`` is the
|
|
12
|
+
real refractive index and ``k`` is the non-negative extinction coefficient.
|
|
13
|
+
The convenience methods ``material.n(wavelength)``, ``material.k(wavelength)``,
|
|
14
|
+
``material.relative_permittivity(wavelength)``, and
|
|
15
|
+
``material.absorption_coefficient(wavelength)`` expose common derived values.
|
|
16
|
+
The relative permittivity is ``N²`` and the intensity absorption coefficient is
|
|
17
|
+
``α = 4πk / λ``.
|
|
18
|
+
|
|
19
|
+
Dispersion and group delay
|
|
20
|
+
--------------------------
|
|
21
|
+
|
|
22
|
+
``compute_group_delay_dispersion`` returns conventional frequency-domain GDD,
|
|
23
|
+
``dτ_g/dω = d²β/dω² × length``. Its units are time squared (typically fs²).
|
|
24
|
+
``compute_group_delay_wavelength_slope`` is deliberately separate because it
|
|
25
|
+
returns ``dτ_g/dλ``, a wavelength-space derivative with different units.
|
|
26
|
+
|
|
27
|
+
Interpolation and validity ranges
|
|
28
|
+
---------------------------------
|
|
29
|
+
|
|
30
|
+
Tabulated materials use linear interpolation by default. Pass
|
|
31
|
+
``interpolation="pchip"`` to ``MaterialPage.load`` or ``TabulatedMaterial`` for
|
|
32
|
+
monotonic piecewise-cubic interpolation without overshoot. Both methods use
|
|
33
|
+
linear endpoint extrapolation when ``out_of_range="warn"``; set
|
|
34
|
+
``out_of_range="raise"`` for strict validity enforcement, or
|
|
35
|
+
``out_of_range="clip"`` to evaluate at the nearest source boundary.
|
|
36
|
+
|
|
37
|
+
Provenance and conditions
|
|
38
|
+
-------------------------
|
|
39
|
+
|
|
40
|
+
Every loaded material provides ``material.provenance``. It contains the source
|
|
41
|
+
reference, upstream catalog ID and URL when available, source path, stated
|
|
42
|
+
wavelength range, and the upstream YAML ``CONDITIONS`` and ``COMMENTS`` fields.
|
|
43
|
+
Use it when recording simulation inputs or preparing results for publication.
|
|
44
|
+
|
|
45
|
+
.. code-block:: python
|
|
46
|
+
|
|
47
|
+
page = catalog.get("specs/SCHOTT-optical/N-BK7")
|
|
48
|
+
material = page.load()
|
|
49
|
+
print(material.provenance)
|
|
@@ -65,6 +65,30 @@ def test_canonical_tabulated_material_and_group_properties(tmp_path):
|
|
|
65
65
|
index = material.compute_refractive_index(wavelengths)
|
|
66
66
|
assert index.shape == wavelengths.shape
|
|
67
67
|
assert np.all(np.isfinite(material.compute_group_index(wavelengths).magnitude))
|
|
68
|
+
wavelength = 0.8 * ureg.micrometer
|
|
69
|
+
assert material.n(wavelength) == pytest.approx(material.compute_refractive_index(wavelength).real)
|
|
70
|
+
assert material.k(wavelength) == pytest.approx(material.compute_refractive_index(wavelength).imag)
|
|
71
|
+
assert material.relative_permittivity(wavelength) == pytest.approx(material.compute_refractive_index(wavelength) ** 2)
|
|
72
|
+
assert material.absorption_coefficient(wavelength).to(1 / ureg.meter).magnitude > 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_tabulated_n_and_k_blocks_support_pchip_and_provenance(tmp_path):
|
|
76
|
+
data_file = tmp_path / "split.yml"
|
|
77
|
+
data_file.write_text(
|
|
78
|
+
"REFERENCES: Example et al. 2026\n"
|
|
79
|
+
"CONDITIONS:\n temperature: 293 K\n"
|
|
80
|
+
"DATA:\n"
|
|
81
|
+
" - type: tabulated n\n data: |\n 0.4 1.4\n 0.8 1.6\n 1.2 1.8\n"
|
|
82
|
+
" - type: tabulated k\n data: |\n 0.4 0.01\n 0.8 0.02\n 1.2 0.03\n"
|
|
83
|
+
)
|
|
84
|
+
from PyOptik.material import TabulatedMaterial
|
|
85
|
+
|
|
86
|
+
material = TabulatedMaterial("split", file_path=data_file, interpolation="pchip")
|
|
87
|
+
index = material.refractive_index(0.6 * ureg.micrometer)
|
|
88
|
+
assert index.real == pytest.approx(1.5)
|
|
89
|
+
assert index.imag == pytest.approx(0.015)
|
|
90
|
+
assert material.provenance["reference"] == "Example et al. 2026"
|
|
91
|
+
assert material.provenance["conditions"]["temperature"] == "293 K"
|
|
68
92
|
|
|
69
93
|
|
|
70
94
|
def test_formula_six_accumulates_all_gas_terms(tmp_path):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|