Skip to content

Commit

Permalink
More options in DispersionFitter and fit constant loss tangent model
Browse files Browse the repository at this point in the history
  • Loading branch information
weiliangjin2021 committed May 3, 2024
1 parent b1fef3f commit 0262c0d
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `JaxSimulation` now supports the following GDS export methods: `to_gds()`, `to_gds_file()`, `to_gdspy()`, and `to_gdstk()`.
- `RunTimeSpec` accepted by `Simulation.run_time` to adaptively set the run time based on Q-factor, propagation length, and other factors.
- `JaxDataArray` now supports selection by nearest value via `JaxDataArray.sel(..., method="nearest")`.
- Convenience method `constant_loss_tangent_model` in `FastDispersionFitter` to fit constant loss tangent material model.
- Classmethods in `DispersionFitter` to load complex-valued permittivity or loss tangent data.

### Changed
- `tidy3d convert` from `.lsf` files to tidy3d scripts is moved to another repository at `https://github.com/hirako22/Lumerical-to-Tidy3D-Converter`.
Expand Down
32 changes: 32 additions & 0 deletions tests/test_plugins/test_dispersion_fitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def test_lossless_dispersion(random_data, mock_remote_api):
fitter = FastDispersionFitter(wvl_um=wvl_um.tolist(), n_data=tuple(n_data))
medium, rms = fitter.fit(advanced_param=advanced_param)

# from permittivity data
fitter = FastDispersionFitter.from_complex_permittivity(wvl_um, n_data**2)
medium2, rms2 = fitter.fit(advanced_param=advanced_param)


@responses.activate
def test_lossy_dispersion(random_data, mock_remote_api):
Expand All @@ -106,6 +110,34 @@ def test_lossy_dispersion(random_data, mock_remote_api):
fitter = FastDispersionFitter(wvl_um=wvl_um.tolist(), n_data=n_data, k_data=k_data)
medium, rms = fitter.fit(advanced_param=advanced_param)

# from permittivity data
eps_complex = (n_data + 1j * k_data) ** 2
fitter = FastDispersionFitter.from_complex_permittivity(
wvl_um, eps_complex.real, eps_complex.imag
)
medium2, rms2 = fitter.fit(advanced_param=advanced_param)

# from loss tangent
fitter = FastDispersionFitter.from_loss_tangent(
wvl_um, eps_complex.real, eps_complex.imag / eps_complex.real
)
medium3, rms3 = fitter.fit(advanced_param=advanced_param)


def test_constant_loss_tangent():
"""perform fitting on constant loss tangent material"""

eps_real = 2.5
loss_tangent = 1e-2
frequency_range = (1e9, 6e9)
mat = FastDispersionFitter.constant_loss_tangent_model(eps_real, loss_tangent, frequency_range)

# validate
sampling_frequency = np.linspace(frequency_range[0], frequency_range[1], 29)
eps_out, loss_tangent_out = mat.loss_tangent_model(sampling_frequency)
assert np.max(np.abs(eps_out - eps_real)) < 2e-2
assert np.max(np.abs(loss_tangent_out - loss_tangent)) / loss_tangent < 2e-2


@responses.activate
def test_dispersion_load_url():
Expand Down
52 changes: 52 additions & 0 deletions tidy3d/components/medium.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,7 @@ def nk_model(self, frequency: float) -> Tuple[float, float]:
----------
frequency : float
Frequency to evaluate permittivity at (Hz).
Returns
-------
Tuple[float, float]
Expand All @@ -771,6 +772,22 @@ def nk_model(self, frequency: float) -> Tuple[float, float]:
eps_complex = self.eps_model(frequency=frequency)
return self.eps_complex_to_nk(eps_complex)

def loss_tangent_model(self, frequency: float) -> Tuple[float, float]:
"""Permittivity and loss tangent as a function of frequency.
Parameters
----------
frequency : float
Frequency to evaluate permittivity at (Hz).
Returns
-------
Tuple[float, float]
Real part of permittivity and loss tangent.
"""
eps_complex = self.eps_model(frequency=frequency)
return self.eps_complex_to_eps_loss_tangent(eps_complex)

@ensure_freq_in_range
def eps_diagonal(self, frequency: float) -> Tuple[complex, complex, complex]:
"""Main diagonal of the complex-valued permittivity tensor as a function of frequency.
Expand Down Expand Up @@ -968,6 +985,41 @@ def eps_complex_to_eps_sigma(eps_complex: complex, freq: float) -> Tuple[float,
sigma = omega * eps_imag * EPSILON_0
return eps_real, sigma

@staticmethod
def eps_complex_to_eps_loss_tangent(eps_complex: complex) -> Tuple[float, float]:
"""Convert complex permittivity to permittivity and loss tangent.
Parameters
----------
eps_complex : complex
Complex-valued relative permittivity.
Returns
-------
Tuple[float, float]
Real part of relative permittivity & loss tangent
"""
eps_real, eps_imag = eps_complex.real, eps_complex.imag
return eps_real, eps_imag / eps_real

@staticmethod
def eps_loss_tangent_to_eps_complex(eps_real: float, loss_tangent: float) -> complex:
"""Convert permittivity and loss tangent to complex permittivity.
Parameters
----------
eps_real : float
Real part of relative permittivity
loss_tangent : float
Loss tangent
Returns
-------
eps_complex : complex
Complex-valued relative permittivity.
"""
return eps_real * (1 + 1j * loss_tangent)

@ensure_freq_in_range
def sigma_model(self, freq: float) -> complex:
"""Complex-valued conductivity as a function of frequency.
Expand Down
69 changes: 67 additions & 2 deletions tidy3d/plugins/dispersion/fit.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Fit PoleResidue Dispersion models to optical NK data
"""
from __future__ import annotations

from typing import Tuple, List, Optional
import csv
Expand Down Expand Up @@ -564,7 +565,9 @@ def _validate_url_load(data_load: List):
raise ValidationError("Invalid URL. Too many k labels.")

@classmethod
def from_url(cls, url_file: str, delimiter: str = ",", ignore_k: bool = False, **kwargs):
def from_url(
cls, url_file: str, delimiter: str = ",", ignore_k: bool = False, **kwargs
) -> DispersionFitter:
"""loads :class:`DispersionFitter` from url linked to a csv/txt file that
contains wavelength (micron), n, and optionally k data. Preferred from
refractiveindex.info.
Expand Down Expand Up @@ -652,7 +655,7 @@ def from_url(cls, url_file: str, delimiter: str = ",", ignore_k: bool = False, *
return cls(wvl_um=n_lam[:, 0], n_data=n_lam[:, 1], **kwargs)

@classmethod
def from_file(cls, fname: str, **loadtxt_kwargs):
def from_file(cls, fname: str, **loadtxt_kwargs) -> DispersionFitter:
"""Loads :class:`DispersionFitter` from file containing wavelength, n, k data.
Parameters
Expand Down Expand Up @@ -697,3 +700,65 @@ def from_file(cls, fname: str, **loadtxt_kwargs):
else:
wvl_um, n_data, k_data = data.T
return cls(wvl_um=wvl_um, n_data=n_data, k_data=k_data)

@classmethod
def from_complex_permittivity(
cls,
wvl_um: ArrayFloat1D,
eps_real: ArrayFloat1D,
eps_imag: ArrayFloat1D = None,
wvl_range: Tuple[Optional[float], Optional[float]] = (None, None),
) -> DispersionFitter:
"""Loads :class:`DispersionFitter` from wavelength and complex relative permittivity data
Parameters
----------
wvl_um : ArrayFloat1D
Wavelength data in micrometers.
eps_real : ArrayFloat1D
Real parts of relative permittivity data
eps_imag : Optional[ArrayFloat1D]
Imaginary parts of relative permittivity data; `None` for lossless medium.
wvg_range : Tuple[Optional[float], Optional[float]]
Wavelength range [wvl_min,wvl_max] for fitting.
Returns
-------
:class:`DispersionFitter`
A :class:`DispersionFitter` instance.
"""
if eps_imag is None:
n, _ = AbstractMedium.eps_complex_to_nk(eps_real + 0j)
return cls(wvl_um=wvl_um, n_data=n, wvl_range=wvl_range)
n, k = AbstractMedium.eps_complex_to_nk(eps_real + eps_imag * 1j)
return cls(wvl_um=wvl_um, n_data=n, k_data=k, wvl_range=wvl_range)

@classmethod
def from_loss_tangent(
cls,
wvl_um: ArrayFloat1D,
eps_real: ArrayFloat1D,
loss_tangent: ArrayFloat1D,
wvl_range: Tuple[Optional[float], Optional[float]] = (None, None),
) -> DispersionFitter:
"""Loads :class:`DispersionFitter` from wavelength and loss tangent data.
Parameters
----------
wvl_um : ArrayFloat1D
Wavelength data in micrometers.
eps_real : ArrayFloat1D
Real parts of relative permittivity data
loss_tangent : Optional[ArrayFloat1D]
Loss tangent data, defined as the ratio of imaginary and real parts of permittivity.
wvl_range : Tuple[Optional[float], Optional[float]]
Wavelength range [wvl_min,wvl_max] for fitting.
Returns
-------
:class:`DispersionFitter`
A :class:`DispersionFitter` instance.
"""
eps_complex = AbstractMedium.eps_loss_tangent_to_eps_complex(eps_real, loss_tangent)
n, k = AbstractMedium.eps_complex_to_nk(eps_complex)
return cls(wvl_um=wvl_um, n_data=n, k_data=k, wvl_range=wvl_range)
46 changes: 46 additions & 0 deletions tidy3d/plugins/dispersion/fit_fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ...components.medium import PoleResidue, LOSS_CHECK_MIN, LOSS_CHECK_MAX, LOSS_CHECK_NUM
from ...components.types import ArrayFloat1D, ArrayComplex1D, ArrayFloat2D, ArrayComplex2D
from ...exceptions import ValidationError
from ...constants import C_0

# numerical tolerance for pole relocation for fast fitter
TOL = 1e-8
Expand Down Expand Up @@ -816,3 +817,48 @@ def make_configs():
)

return best_model.pole_residue, best_model.rms_error

@classmethod
def constant_loss_tangent_model(
cls,
eps_real: float,
loss_tangent: float,
frequency_range: Tuple[float, float],
max_num_poles: PositiveInt = DEFAULT_MAX_POLES,
number_sampling_frequency: PositiveInt = 10,
tolerance_rms: NonNegativeFloat = DEFAULT_TOLERANCE_RMS,
) -> PoleResidue:
"""Fit a constant loss tangent material model.
Parameters
----------
eps_real : float
Real part of permittivity
loss_tangent : float
Loss tangent.
frequency_range : Tuple[float, float]
Freqquency range for the material to exhibit constant loss tangent response.
max_num_poles : PositiveInt, optional
Maximum number of poles in the model.
number_sampling_frequency : PositiveInt, optional
Number of sampling frequencies to compute RMS error for fitting.
tolerance_rms : float, optional
Weighted RMS error below which the fit is successful and the result is returned.
Returns
-------
:class:`.PoleResidue
Best results of multiple fits.
"""
if number_sampling_frequency < 2:
frequencies = np.array([np.mean(frequency_range)])
else:
frequencies = np.linspace(
frequency_range[0], frequency_range[1], number_sampling_frequency
)
wvl_um = C_0 / frequencies
eps_real_array = np.ones_like(frequencies) * eps_real
loss_tangent_array = np.ones_like(frequencies) * loss_tangent
fitter = cls.from_loss_tangent(wvl_um, eps_real_array, loss_tangent_array)
material, _ = fitter.fit(max_num_poles=max_num_poles, tolerance_rms=tolerance_rms)
return material

0 comments on commit 0262c0d

Please sign in to comment.