Skip to content

Commit 6f783bd

Browse files
More robust Sellmeier and Debye; validate PoleResidue parameters
1 parent 67edb48 commit 6f783bd

File tree

5 files changed

+99
-19
lines changed

5 files changed

+99
-19
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
### Changed
1212
- Improved performance of antenna metrics calculation by utilizing cached wave amplitude calculations instead of recomputing wave amplitudes for each port excitation in the `TerminalComponentModelerData`.
1313

14+
### Fixed
15+
- More robust `Sellmeier` and `Debye` material model, and prevent very large pole parameters in `PoleResidue` material model.
16+
17+
1418
## [v2.10.0rc2] - 2025-10-01
1519

1620
### Added

tests/test_components/test_custom.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,14 @@ def make_custom_current_source():
7272
return td.CustomCurrentSource(size=SIZE, source_time=ST, current_dataset=current_dataset)
7373

7474

75-
def make_spatial_data(value=0, dx=0, unstructured=False, seed=None, uniform=False):
75+
def make_spatial_data(
76+
value=0, dx=0, unstructured=False, seed=None, uniform=False, random_magnitude=1
77+
):
7678
"""Makes a spatial data array."""
7779
if uniform:
7880
data = value * np.ones((Nx, Ny, Nz))
7981
else:
80-
data = np.random.random((Nx, Ny, Nz)) + value
82+
data = np.random.random((Nx, Ny, Nz)) * random_magnitude + value
8183
arr = td.SpatialDataArray(data, coords={"x": X + dx, "y": Y, "z": Z})
8284
if unstructured:
8385
method = "direct" if uniform else "linear"
@@ -777,11 +779,11 @@ def test_custom_pole_residue(unstructured):
777779
def test_custom_sellmeier(unstructured):
778780
"""Custom Sellmeier medium."""
779781
seed = 897245
780-
b1 = make_spatial_data(value=0, unstructured=unstructured, seed=seed)
781-
c1 = make_spatial_data(value=0, unstructured=unstructured, seed=seed)
782+
b1 = make_spatial_data(value=0.1, unstructured=unstructured, seed=seed)
783+
c1 = make_spatial_data(value=0.1, unstructured=unstructured, seed=seed)
782784

783-
b2 = make_spatial_data(value=0, unstructured=unstructured, seed=seed)
784-
c2 = make_spatial_data(value=0, unstructured=unstructured, seed=seed)
785+
b2 = make_spatial_data(value=0.1, unstructured=unstructured, seed=seed)
786+
c2 = make_spatial_data(value=0.1, unstructured=unstructured, seed=seed)
785787

786788
# complex b
787789
with pytest.raises(pydantic.ValidationError):
@@ -815,6 +817,13 @@ def test_custom_sellmeier(unstructured):
815817
btmp = make_spatial_data(value=0, dx=1, unstructured=(not unstructured), seed=seed)
816818
mat = CustomSellmeier(coeffs=((b1, c2), (btmp, c2)))
817819

820+
# some of C is close to 0
821+
with pytest.raises(pydantic.ValidationError):
822+
ctmp = make_spatial_data(
823+
value=0, unstructured=unstructured, seed=seed, random_magnitude=1e-7
824+
)
825+
mat = CustomSellmeier(coeffs=((b1, c1), (b2, ctmp)))
826+
818827
mat = CustomSellmeier(coeffs=((b1, c1), (b2, c2)))
819828
verify_custom_dispersive_medium_methods(mat, ["coeffs"])
820829
assert mat.n_cfl == 1
@@ -931,10 +940,10 @@ def test_custom_debye(unstructured):
931940
eps_inf = make_spatial_data(value=1, unstructured=unstructured, seed=seed)
932941

933942
eps1 = make_spatial_data(value=0, unstructured=unstructured, seed=seed)
934-
tau1 = make_spatial_data(value=0, unstructured=unstructured, seed=seed)
943+
tau1 = make_spatial_data(value=0.1, unstructured=unstructured, seed=seed)
935944

936945
eps2 = make_spatial_data(value=0, unstructured=unstructured, seed=seed)
937-
tau2 = make_spatial_data(value=0, unstructured=unstructured, seed=seed)
946+
tau2 = make_spatial_data(value=0.1, unstructured=unstructured, seed=seed)
938947

939948
# complex eps
940949
with pytest.raises(pydantic.ValidationError):
@@ -951,6 +960,12 @@ def test_custom_debye(unstructured):
951960
tautmp = make_spatial_data(value=-0.5, unstructured=unstructured, seed=seed)
952961
mat = CustomDebye(eps_inf=eps_inf, coeffs=((eps1, tau1), (eps2, tautmp)))
953962

963+
# some of tau is close to 0
964+
with pytest.raises(pydantic.ValidationError):
965+
tautmp = make_spatial_data(
966+
value=0, unstructured=unstructured, seed=seed, random_magnitude=1e-38
967+
)
968+
mat = CustomDebye(eps_inf=eps_inf, coeffs=((eps1, tau1), (eps2, tautmp)))
954969
# inconsistent coords
955970
with pytest.raises(pydantic.ValidationError):
956971
epstmp = make_spatial_data(value=0, dx=1, unstructured=unstructured, seed=seed)

tests/test_components/test_medium.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ def test_medium():
6262
_ = td.Medium(conductivity=-1.0)
6363

6464

65+
def test_validate_largest_pole_parameters():
66+
# error for large pole parameters
67+
with pytest.raises(pydantic.ValidationError):
68+
_ = td.PoleResidue(poles=[((-1e50 + 2j), (1 + 3j))])
69+
70+
with pytest.raises(pydantic.ValidationError):
71+
_ = td.PoleResidue(poles=[((-1 + 2j), (1e50 + 3j))])
72+
73+
6574
def test_medium_conversions():
6675
n = 4.0
6776
k = 1.0
@@ -255,13 +264,15 @@ def test_medium_dispersion():
255264
def test_medium_dispersion_conversion():
256265
m_PR = td.PoleResidue(eps_inf=1.0, poles=[((-1 + 2j), (1 + 3j)), ((-2 + 4j), (1 + 5j))])
257266
m_SM = td.Sellmeier(coeffs=[(2, 3), (2, 4)])
267+
m_SM_small_C = td.Sellmeier(coeffs=[(2, 3), (2, 1e-20)])
258268
m_LZ = td.Lorentz(eps_inf=1.0, coeffs=[(1, 3, 2), (2, 4, 1)])
259269
m_LZ2 = td.Lorentz(eps_inf=1.0, coeffs=[(1, 2, 3), (2, 1, 4)])
260270
m_DR = td.Drude(eps_inf=1.0, coeffs=[(1, 3), (2, 4)])
261271
m_DB = td.Debye(eps_inf=1.0, coeffs=[(1, 3), (2, 4)])
272+
m_DB_small_tau = td.Debye(eps_inf=1.0, coeffs=[(1, 3), (2, 1e-50)])
262273

263274
freqs = np.linspace(0.01, 1, 1001)
264-
for medium in [m_PR, m_SM, m_DB, m_LZ, m_DR, m_LZ2]: # , m_DB]:
275+
for medium in [m_PR, m_SM, m_SM_small_C, m_DB, m_DB_small_tau, m_LZ, m_DR, m_LZ2]: # , m_DB]:
265276
eps_model = medium.eps_model(freqs)
266277
eps_pr = medium.pole_residue.eps_model(freqs)
267278
np.testing.assert_allclose(eps_model, eps_pr)

tidy3d/components/medium.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
ETA_0,
2525
HBAR,
2626
HERTZ,
27+
LARGEST_FP_NUMBER,
2728
MICROMETER,
2829
MU_0,
2930
PERMITTIVITY,
@@ -3367,6 +3368,18 @@ def _causality_validation(cls, val):
33673368
raise SetupError("For stable medium, 'Re(a_i)' must be non-positive.")
33683369
return val
33693370

3371+
@pd.validator("poles", always=True)
3372+
def _poles_largest_value(cls, val):
3373+
"""Assert pole parameters are not too large."""
3374+
for a, c in val:
3375+
if np.any(abs(_get_numpy_array(a)) > LARGEST_FP_NUMBER):
3376+
raise ValidationError(
3377+
"The value of some 'a_i' is too large. They are unlikely to contribute to material dispersion."
3378+
)
3379+
if np.any(abs(_get_numpy_array(c)) > LARGEST_FP_NUMBER):
3380+
raise ValidationError("The value of some 'c_i' is too large.")
3381+
return val
3382+
33703383
_validate_permittivity_modulation = DispersiveMedium._permittivity_modulation_validation()
33713384
_validate_conductivity_modulation = DispersiveMedium._conductivity_modulation_validation()
33723385

@@ -4261,14 +4274,19 @@ def eps_model(self, frequency: float) -> complex:
42614274
def _pole_residue_dict(self) -> dict:
42624275
"""Dict representation of Medium as a pole-residue model"""
42634276
poles = []
4277+
eps_inf = _ones_like(self.coeffs[0][0])
42644278
for B, C in self.coeffs:
4265-
beta = 2 * np.pi * C_0 / np.sqrt(C)
4266-
alpha = -0.5 * beta * B
4267-
a = 1j * beta
4268-
c = 1j * alpha
4269-
poles.append((a, c))
4279+
# for small C, it's equivalent to modifying eps_inf
4280+
if np.any(np.isclose(_get_numpy_array(C), 0)):
4281+
eps_inf += B
4282+
else:
4283+
beta = 2 * np.pi * C_0 / np.sqrt(C)
4284+
alpha = -0.5 * beta * B
4285+
a = 1j * beta
4286+
c = 1j * alpha
4287+
poles.append((a, c))
42704288
return {
4271-
"eps_inf": 1,
4289+
"eps_inf": eps_inf,
42724290
"poles": poles,
42734291
"frequency_range": self.frequency_range,
42744292
"name": self.name,
@@ -4439,6 +4457,18 @@ def _passivity_validation(cls, val, values):
44394457
)
44404458
return val
44414459

4460+
@pd.validator("coeffs", always=True)
4461+
def _coeffs_C_all_near_zero_or_much_greater(cls, val):
4462+
"""We restrict either all C~=0, or very different from 0."""
4463+
for _, C in val:
4464+
c_array_near_zero = np.isclose(_get_numpy_array(C), 0)
4465+
if np.any(c_array_near_zero) and not np.all(c_array_near_zero):
4466+
raise SetupError(
4467+
"Coefficients 'C_i' are restricted to be "
4468+
"either all near zero or much greater than 0."
4469+
)
4470+
return val
4471+
44424472
@cached_property
44434473
def is_spatially_uniform(self) -> bool:
44444474
"""Whether the medium is spatially uniform."""
@@ -5546,14 +5576,19 @@ def _pole_residue_dict(self):
55465576
"""Dict representation of Medium as a pole-residue model."""
55475577

55485578
poles = []
5579+
eps_inf = self.eps_inf
55495580
for de, tau in self.coeffs:
5550-
a = -2 * np.pi / tau + 0j
5551-
c = -0.5 * de * a
5581+
# for |tau| close to 0, it's equivalent to modifying eps_inf
5582+
if np.any(abs(_get_numpy_array(tau)) < 1 / 2 / np.pi / LARGEST_FP_NUMBER):
5583+
eps_inf = eps_inf + de
5584+
else:
5585+
a = -2 * np.pi / tau + 0j
5586+
c = -0.5 * de * a
55525587

5553-
poles.append((a, c))
5588+
poles.append((a, c))
55545589

55555590
return {
5556-
"eps_inf": self.eps_inf,
5591+
"eps_inf": eps_inf,
55575592
"poles": poles,
55585593
"frequency_range": self.frequency_range,
55595594
"name": self.name,
@@ -5689,6 +5724,16 @@ def _coeffs_correct_shape(cls, val, values):
56895724
raise SetupError("All terms in 'coeffs' must be real.")
56905725
return val
56915726

5727+
@pd.validator("coeffs", always=True)
5728+
def _coeffs_tau_all_sufficient_positive(cls, val):
5729+
"""We restrict either all tau is sufficently greater than 0."""
5730+
for _, tau in val:
5731+
if np.any(_get_numpy_array(tau) < 1 / 2 / np.pi / LARGEST_FP_NUMBER):
5732+
raise SetupError(
5733+
"Coefficients 'tau_i' are restricted to be sufficiently greater than 0."
5734+
)
5735+
return val
5736+
56925737
def _compute_derivatives(self, derivative_info: DerivativeInfo) -> AutogradFieldMap:
56935738
"""Adjoint derivatives for CustomDebye via analytic chain rule."""
56945739

tidy3d/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@
261261
Large number used for comparing infinity.
262262
"""
263263

264+
LARGEST_FP_NUMBER = 1e38
265+
"""
266+
Largest number used for single precision floating point number.
267+
"""
268+
264269
inf = np.inf
265270
"""
266271
Representation of infinity used within tidy3d.

0 commit comments

Comments
 (0)