Skip to content

Commit 10dbb24

Browse files
committed
feat: add built-in material cards
1 parent dd2f8dc commit 10dbb24

3 files changed

Lines changed: 272 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies = [
1818
"meshio>=5.0.0",
1919
"nbformat>=4.2.0",
2020
"nest_asyncio2",
21+
"pdk-schema==0.0.2",
2122
"plotly",
2223
"pyvista>=0.43.0",
2324
"trame",

src/gsim/common/materials.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""Built-in optical material cards."""
2+
3+
from collections.abc import Mapping
4+
5+
from pdk_schema import (
6+
Band,
7+
Coord,
8+
Index,
9+
MaterialCard,
10+
Provenance,
11+
Regime,
12+
Sellmeier,
13+
SellmeierTerm,
14+
TableData,
15+
TabulatedValue,
16+
Validity,
17+
)
18+
19+
_LI_293K_DATA = (
20+
(1.20, 3.5167),
21+
(1.22, 3.5133),
22+
(1.24, 3.5102),
23+
(1.26, 3.5072),
24+
(1.28, 3.5043),
25+
(1.30, 3.5016),
26+
(1.32, 3.4990),
27+
(1.34, 3.4965),
28+
(1.36, 3.4941),
29+
(1.38, 3.4918),
30+
(1.40, 3.4896),
31+
(1.45, 3.4845),
32+
(1.50, 3.4799),
33+
(1.55, 3.4757),
34+
(1.60, 3.4719),
35+
(1.65, 3.4684),
36+
(1.70, 3.4653),
37+
(1.80, 3.4597),
38+
(1.90, 3.4550),
39+
(2.00, 3.4510),
40+
(2.25, 3.4431),
41+
(2.50, 3.4375),
42+
(2.75, 3.4334),
43+
(3.00, 3.4302),
44+
(4.00, 3.4229),
45+
(5.00, 3.4195),
46+
(6.00, 3.4177),
47+
(7.00, 3.4165),
48+
(8.00, 3.4158),
49+
(9.00, 3.4153),
50+
(10.0, 3.4150),
51+
(11.0, 3.4147),
52+
(12.0, 3.4145),
53+
(13.0, 3.4144),
54+
(14.0, 3.4142),
55+
)
56+
57+
58+
def _validity(minimum_um: float, maximum_um: float) -> Validity:
59+
"""Return a strict wavelength validity range in micrometers."""
60+
return Validity(
61+
at=None,
62+
over={
63+
"wavelength": Band(
64+
min=minimum_um,
65+
max=maximum_um,
66+
unit="um",
67+
label=None,
68+
)
69+
},
70+
on_out_of_range="raise",
71+
)
72+
73+
74+
def _provenance(label: str) -> Provenance:
75+
"""Return the minimum required literature provenance."""
76+
return Provenance(
77+
source="literature",
78+
label=label,
79+
maturity="empirical",
80+
citations=[],
81+
comment=None,
82+
url=None,
83+
data_url=None,
84+
info={},
85+
)
86+
87+
88+
def _card(
89+
name: str,
90+
permittivity: Index | Sellmeier,
91+
temperature_ref: float,
92+
) -> MaterialCard:
93+
"""Build an optical material card."""
94+
return MaterialCard(
95+
name=name,
96+
optical=Regime(
97+
temperature_ref=temperature_ref,
98+
provenance=_provenance(name),
99+
permittivity=permittivity,
100+
conductivity=None,
101+
permeability=None,
102+
perturbations=[],
103+
info={},
104+
),
105+
rf=None,
106+
info={},
107+
)
108+
109+
110+
_SI_SALZBERG = _card(
111+
name="Si-Salzberg",
112+
temperature_ref=299.15,
113+
permittivity=Sellmeier(
114+
validity=_validity(1.357, 11.04),
115+
variation=None,
116+
conductivity=None,
117+
terms=(
118+
SellmeierTerm(b=10.6684293, c_um=0.301516485),
119+
SellmeierTerm(b=0.0030434748, c_um=1.13475115),
120+
SellmeierTerm(b=1.54133408, c_um=1104.0),
121+
),
122+
offset=0.0,
123+
),
124+
)
125+
126+
_SI_LI_293K = _card(
127+
name="Si-Li-293K",
128+
temperature_ref=293.0,
129+
permittivity=Index(
130+
validity=_validity(1.2, 14.0),
131+
variation=None,
132+
conductivity=None,
133+
n=TabulatedValue(
134+
unit="",
135+
data=TableData(
136+
dims=("wavelength",),
137+
coords={
138+
"wavelength": Coord(
139+
values=[row[0] for row in _LI_293K_DATA],
140+
unit="um",
141+
)
142+
},
143+
values=[row[1] for row in _LI_293K_DATA],
144+
attrs={},
145+
interp="linear",
146+
),
147+
),
148+
k=None,
149+
),
150+
)
151+
152+
_SIO2_MALITSON = _card(
153+
name="SiO2-Malitson",
154+
temperature_ref=293.0,
155+
permittivity=Sellmeier(
156+
validity=_validity(0.21, 6.7),
157+
variation=None,
158+
conductivity=None,
159+
terms=(
160+
SellmeierTerm(b=0.6961663, c_um=0.0684043),
161+
SellmeierTerm(b=0.4079426, c_um=0.1162414),
162+
SellmeierTerm(b=0.8974794, c_um=9.896161),
163+
),
164+
offset=0.0,
165+
),
166+
)
167+
168+
GSIM_MATERIAL_CARDS: dict[str, MaterialCard] = {
169+
"Si": _SI_SALZBERG.model_copy(update={"name": "Si"}),
170+
"Si-Salzberg": _SI_SALZBERG,
171+
"Si-Li-293K": _SI_LI_293K,
172+
"SiO2": _SIO2_MALITSON.model_copy(update={"name": "SiO2"}),
173+
"SiO2-Malitson": _SIO2_MALITSON,
174+
}
175+
176+
177+
def get_material_card(
178+
material_name: str,
179+
project_material_cards: Mapping[str, MaterialCard] | None = None,
180+
) -> MaterialCard:
181+
"""Return a project card when present, otherwise a built-in card."""
182+
if project_material_cards and material_name in project_material_cards:
183+
return project_material_cards[material_name]
184+
return GSIM_MATERIAL_CARDS[material_name]
185+
186+
187+
__all__ = ["GSIM_MATERIAL_CARDS", "get_material_card"]
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from math import sqrt
2+
3+
import pytest
4+
from pdk_schema import Index, MaterialCard, Sellmeier, TabulatedValue
5+
6+
from gsim.common.materials import GSIM_MATERIAL_CARDS, get_material_card
7+
8+
9+
def _sellmeier_index(model: Sellmeier, wavelength_um: float) -> float:
10+
wavelength_squared = wavelength_um**2
11+
index_squared = (
12+
1.0
13+
+ model.offset
14+
+ sum(
15+
term.b * wavelength_squared / (wavelength_squared - term.c_um**2)
16+
for term in model.terms
17+
)
18+
)
19+
return sqrt(index_squared)
20+
21+
22+
def test_material_card_names() -> None:
23+
assert set(GSIM_MATERIAL_CARDS) == {
24+
"Si",
25+
"Si-Salzberg",
26+
"Si-Li-293K",
27+
"SiO2",
28+
"SiO2-Malitson",
29+
}
30+
assert all(name == card.name for name, card in GSIM_MATERIAL_CARDS.items())
31+
32+
33+
def test_silicon_fallback_uses_salzberg() -> None:
34+
fallback = GSIM_MATERIAL_CARDS["Si"]
35+
named = GSIM_MATERIAL_CARDS["Si-Salzberg"]
36+
37+
assert fallback.optical == named.optical
38+
assert fallback.optical is not None
39+
assert isinstance(fallback.optical.permittivity, Sellmeier)
40+
assert _sellmeier_index(fallback.optical.permittivity, 1.55) == pytest.approx(
41+
3.477723756,
42+
abs=1e-9,
43+
)
44+
45+
46+
def test_li_293k_data() -> None:
47+
card = GSIM_MATERIAL_CARDS["Si-Li-293K"]
48+
49+
assert card.optical is not None
50+
assert isinstance(card.optical.permittivity, Index)
51+
assert isinstance(card.optical.permittivity.n, TabulatedValue)
52+
table = card.optical.permittivity.n.data
53+
telecom_row = table.coords["wavelength"].values.index(1.55)
54+
assert len(table.values) == 35
55+
assert table.values[telecom_row] == 3.4757
56+
57+
58+
def test_silicon_dioxide_fallback_uses_malitson() -> None:
59+
fallback = GSIM_MATERIAL_CARDS["SiO2"]
60+
named = GSIM_MATERIAL_CARDS["SiO2-Malitson"]
61+
62+
assert fallback.optical == named.optical
63+
assert fallback.optical is not None
64+
assert isinstance(fallback.optical.permittivity, Sellmeier)
65+
assert _sellmeier_index(fallback.optical.permittivity, 1.55) == pytest.approx(
66+
1.444023622,
67+
abs=1e-9,
68+
)
69+
70+
71+
def test_project_material_card_takes_precedence() -> None:
72+
project_card = MaterialCard(name="Si", optical=None, rf=None, info={})
73+
74+
assert get_material_card("Si", {"Si": project_card}) is project_card
75+
assert (
76+
get_material_card("SiO2", {"Si": project_card}) is GSIM_MATERIAL_CARDS["SiO2"]
77+
)
78+
79+
80+
def test_cards_have_no_external_data_references() -> None:
81+
for card in GSIM_MATERIAL_CARDS.values():
82+
assert card.optical is not None
83+
assert card.optical.provenance.url is None
84+
assert card.optical.provenance.data_url is None

0 commit comments

Comments
 (0)