diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd890528..e806cbf6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/tests/test_plugins/test_dispersion_fitter.py b/tests/test_plugins/test_dispersion_fitter.py index 7a389d91c..5dbe2e12b 100644 --- a/tests/test_plugins/test_dispersion_fitter.py +++ b/tests/test_plugins/test_dispersion_fitter.py @@ -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): @@ -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(): diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index 18a7e7167..b9623ed57 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -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] @@ -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. @@ -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. diff --git a/tidy3d/plugins/dispersion/fit.py b/tidy3d/plugins/dispersion/fit.py index 10b2602b7..335a7f65a 100644 --- a/tidy3d/plugins/dispersion/fit.py +++ b/tidy3d/plugins/dispersion/fit.py @@ -1,5 +1,6 @@ """Fit PoleResidue Dispersion models to optical NK data """ +from __future__ import annotations from typing import Tuple, List, Optional import csv @@ -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. @@ -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 @@ -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) diff --git a/tidy3d/plugins/dispersion/fit_fast.py b/tidy3d/plugins/dispersion/fit_fast.py index 72ea60fb5..fe8db8436 100644 --- a/tidy3d/plugins/dispersion/fit_fast.py +++ b/tidy3d/plugins/dispersion/fit_fast.py @@ -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 @@ -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