Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More options in DispersionFitter and fit constant loss tangent model #1652

Merged
merged 1 commit into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
weiliangjin2021 marked this conversation as resolved.
Show resolved Hide resolved
-------
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
Loading