Skip to content

Commit fc2ba3e

Browse files
from_nk to support arbitrary nk values
1 parent a535435 commit fc2ba3e

File tree

4 files changed

+140
-1
lines changed

4 files changed

+140
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Added
99
- `tidy3d.plugins.design` tool to explore user-defined design spaces.
1010
- `ModeData.dispersion` and `ModeSolverData.dispersion` are calculated together with the group index.
11+
- A utility function `td.medium_from_nk()` that automatically constructs a dispersivless medium when permittivity>=1, and a single-pole Lorentz medium when permittivity<1.
1112

1213
### Changed
1314

tests/test_components/test_medium.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,60 @@ def test_medium_conversions():
8181
assert np.isclose(k, k_)
8282

8383

84+
def test_lorentz_medium_conversions(log_capture):
85+
freq = 3.0
86+
87+
# lossless, eps_r > 1
88+
eps_complex = 2 + 0j
89+
n, k = td.Lorentz.eps_complex_to_nk(eps_complex)
90+
medium = td.Lorentz.from_nk(n, k, freq)
91+
assert_log_level(log_capture, "WARNING")
92+
eps_model = medium.eps_model(freq)
93+
assert np.isclose(eps_complex, eps_model)
94+
95+
# lossless, eps_r < 1
96+
eps_complex = 0.5 + 0j
97+
n, k = td.Lorentz.eps_complex_to_nk(eps_complex)
98+
medium = td.Lorentz.from_nk(n, k, freq)
99+
eps_model = medium.eps_model(freq)
100+
assert np.isclose(eps_complex, eps_model)
101+
102+
# lossy, eps_r < 1
103+
eps_complex = 0.5 + 0.1j
104+
n, k = td.Lorentz.eps_complex_to_nk(eps_complex)
105+
medium = td.Lorentz.from_nk(n, k, freq)
106+
eps_model = medium.eps_model(freq)
107+
assert np.isclose(eps_complex, eps_model)
108+
109+
# lossy, eps_r > 1
110+
eps_complex = 1.5 + 2j
111+
n, k = td.Lorentz.eps_complex_to_nk(eps_complex)
112+
medium = td.Lorentz.from_nk(n, k, freq)
113+
assert_log_level(log_capture, "WARNING")
114+
eps_model = medium.eps_model(freq)
115+
assert np.isclose(eps_complex, eps_model)
116+
117+
118+
def test_medium_from_nk():
119+
freq = 3.0
120+
121+
# lossy, eps_r < 1
122+
eps_complex = 0.5 + 0.1j
123+
n, k = td.AbstractMedium.eps_complex_to_nk(eps_complex)
124+
medium = td.medium_from_nk(n, k, freq)
125+
eps_model = medium.eps_model(freq)
126+
assert np.isclose(eps_complex, eps_model)
127+
assert medium.type == "Lorentz"
128+
129+
# lossy, eps_r > 1
130+
eps_complex = 1.5 + 2j
131+
n, k = td.AbstractMedium.eps_complex_to_nk(eps_complex)
132+
medium = td.medium_from_nk(n, k, freq)
133+
eps_model = medium.eps_model(freq)
134+
assert np.isclose(eps_complex, eps_model)
135+
assert medium.type == "Medium"
136+
137+
84138
def test_PEC():
85139

86140
_ = td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=td.PEC)

tidy3d/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
# medium
1414
from .components.medium import Medium, PoleResidue, AnisotropicMedium, PEC, PECMedium
15-
from .components.medium import Medium2D, PEC2D
15+
from .components.medium import Medium2D, PEC2D, medium_from_nk
1616
from .components.medium import Sellmeier, Debye, Drude, Lorentz
1717
from .components.medium import CustomMedium, CustomPoleResidue
1818
from .components.medium import CustomSellmeier, FullyAnisotropicMedium
@@ -326,4 +326,5 @@ def set_logging_level(level: str) -> None:
326326
"IndexedDataArray",
327327
"TriangularGridDataset",
328328
"TetrahedralGridDataset",
329+
"medium_from_nk",
329330
]

tidy3d/components/medium.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,12 @@ def from_nk(cls, n: float, k: float, freq: float, **kwargs):
11381138
medium containing the corresponding ``permittivity`` and ``conductivity``.
11391139
"""
11401140
eps, sigma = AbstractMedium.nk_to_eps_sigma(n, k, freq)
1141+
if eps < 1:
1142+
raise ValidationError(
1143+
"Dispersiveless medium must have 'permittivity>=1`. "
1144+
"Please use 'Lorentz.from_nk()' to covert to a Lorentz medium, or the utility "
1145+
"function 'td.medium_from_nk()' to automatically return the proper medium type."
1146+
)
11411147
return cls(permittivity=eps, conductivity=sigma, **kwargs)
11421148

11431149

@@ -2998,6 +3004,59 @@ def _all_larger(coeff_a, coeff_b) -> bool:
29983004
return np.all(coeff_a.values > coeff_b.values)
29993005
return coeff_a > coeff_b
30003006

3007+
@classmethod
3008+
def from_nk(cls, n: float, k: float, freq: float, **kwargs):
3009+
"""Convert ``n`` and ``k`` values at frequency ``freq`` to a single-pole Lorentz
3010+
medium.
3011+
3012+
Parameters
3013+
----------
3014+
n : float
3015+
Real part of refractive index.
3016+
k : float = 0
3017+
Imaginary part of refrative index.
3018+
freq : float
3019+
Frequency to evaluate permittivity at (Hz).
3020+
3021+
Returns
3022+
-------
3023+
:class:`Lorentz`
3024+
Lorentz medium having refractive index n+ik at frequency ``freq``.
3025+
"""
3026+
eps_complex = AbstractMedium.nk_to_eps_complex(n, k)
3027+
eps_r, eps_i = eps_complex.real, eps_complex.imag
3028+
if eps_r >= 1:
3029+
log.warning(
3030+
"For 'permittivity>=1', it is more computationally efficient to "
3031+
"use a dispersiveless medium constructed from 'Medium.from_nk()'."
3032+
)
3033+
# first, lossless medium
3034+
if isclose(eps_i, 0):
3035+
if eps_r < 1:
3036+
fp = np.sqrt((eps_r - 1) / (eps_r - 2)) * freq
3037+
return cls(
3038+
eps_inf=1,
3039+
coeffs=[
3040+
(1, fp, 0),
3041+
],
3042+
)
3043+
return cls(
3044+
eps_inf=1,
3045+
coeffs=[
3046+
((eps_r - 1) / 2, np.sqrt(2) * freq, 0),
3047+
],
3048+
)
3049+
# lossy medium
3050+
alpha = (eps_r - 1) / eps_i
3051+
delta_p = freq / 2 / (alpha**2 - alpha + 1)
3052+
fp = np.sqrt((alpha**2 + 1) / (alpha**2 - alpha + 1)) * freq
3053+
return cls(
3054+
eps_inf=1,
3055+
coeffs=[
3056+
(eps_i, fp, delta_p),
3057+
],
3058+
)
3059+
30013060

30023061
class CustomLorentz(CustomDispersiveMedium, Lorentz):
30033062
"""A spatially varying dispersive medium described by the Lorentz model.
@@ -4843,3 +4902,27 @@ def is_comp_pec_2d(self, comp: Axis, axis: Axis):
48434902
# types of mediums that can be used in Simulation and Structures
48444903

48454904
MediumType = Union[MediumType3D, Medium2D]
4905+
4906+
# Utility function
4907+
def medium_from_nk(n: float, k: float, freq: float, **kwargs) -> Union[Medium, Lorentz]:
4908+
"""Convert ``n`` and ``k`` values at frequency ``freq`` to :class:`Medium` if ``Re[epsilon]>=1``,
4909+
or :class:`Lorentz` if if ``Re[epsilon]<1``.
4910+
4911+
Parameters
4912+
----------
4913+
n : float
4914+
Real part of refractive index.
4915+
k : float = 0
4916+
Imaginary part of refrative index.
4917+
freq : float
4918+
Frequency to evaluate permittivity at (Hz).
4919+
4920+
Returns
4921+
-------
4922+
Union[:class:`Medium`, :class:`Lorentz`]
4923+
Dispersionless medium or Lorentz medium having refractive index n+ik at frequency ``freq``.
4924+
"""
4925+
eps_complex = AbstractMedium.nk_to_eps_complex(n, k)
4926+
if eps_complex.real >= 1:
4927+
return Medium.from_nk(n, k, freq, **kwargs)
4928+
return Lorentz.from_nk(n, k, freq, **kwargs)

0 commit comments

Comments
 (0)