|
6 | 6 |
|
7 | 7 | from typing import Literal, Optional |
8 | 8 |
|
| 9 | +import numpy as np |
9 | 10 | import pydantic.v1 as pd |
10 | 11 | import xarray as xr |
11 | 12 | from typing_extensions import Self |
12 | 13 |
|
13 | | -from tidy3d.components.data.data_array import FieldProjectionAngleDataArray, FreqDataArray |
| 14 | +from tidy3d.components.data.data_array import ( |
| 15 | + FieldProjectionAngleDataArray, |
| 16 | + FreqDataArray, |
| 17 | + FreqModeDataArray, |
| 18 | + ImpedanceFreqModeDataArray, |
| 19 | +) |
14 | 20 | from tidy3d.components.data.monitor_data import DirectivityData, ModeData, ModeSolverData |
15 | 21 | from tidy3d.components.microwave.base import MicrowaveBaseModel |
| 22 | +from tidy3d.components.microwave.data.data_array import ( |
| 23 | + AttenuationConstantArray, |
| 24 | + GroupVelocityArray, |
| 25 | + PhaseConstantArray, |
| 26 | + PhaseVelocityArray, |
| 27 | + PropagationConstantArray, |
| 28 | +) |
16 | 29 | from tidy3d.components.microwave.data.dataset import TransmissionLineDataset |
17 | 30 | from tidy3d.components.microwave.monitor import MicrowaveModeMonitor, MicrowaveModeSolverMonitor |
18 | | -from tidy3d.components.types import FreqArray, PolarizationBasis |
| 31 | +from tidy3d.components.types import FreqArray, ModeClassification, PolarizationBasis |
| 32 | +from tidy3d.constants import C_0 |
| 33 | +from tidy3d.log import log |
19 | 34 |
|
20 | 35 |
|
21 | 36 | class AntennaMetricsData(DirectivityData, MicrowaveBaseModel): |
@@ -242,6 +257,160 @@ def modes_info(self) -> xr.Dataset: |
242 | 257 | super_info["Im(Z0)"] = self.transmission_line_data.Z0.imag |
243 | 258 | return super_info |
244 | 259 |
|
| 260 | + @property |
| 261 | + def mode_classifications(self) -> list[ModeClassification]: |
| 262 | + """List of mode classifications (TEM, quasi-TEM, TE, TM, or Hybrid) for each mode.""" |
| 263 | + return [self._classify_mode(mode_index) for mode_index in self.n_complex.mode_index] |
| 264 | + |
| 265 | + @property |
| 266 | + def free_space_wavenumber(self) -> FreqDataArray: |
| 267 | + """The free space wavenumber (k_0) in rad/m.""" |
| 268 | + freqs = self.n_complex.f.values |
| 269 | + C_0_meters = C_0 * 1e-6 |
| 270 | + return FreqDataArray(2 * np.pi * freqs / C_0_meters, coords={"f": freqs}) |
| 271 | + |
| 272 | + @property |
| 273 | + def gamma(self) -> PropagationConstantArray: |
| 274 | + r"""The propagation constant with SI units. |
| 275 | +
|
| 276 | + In the physics convention, where time-harmonic fields evolve with :math:`e^{-\omega t}`, |
| 277 | + a wave propagating in the +z direction varies as: |
| 278 | +
|
| 279 | + .. math:: |
| 280 | +
|
| 281 | + E(z) = E_0 e^{\gamma z} = E_0 e^{-\alpha z} e^{j\beta z} |
| 282 | +
|
| 283 | + where :math:`\gamma = -\alpha + j\beta`. |
| 284 | + """ |
| 285 | + data = 1j * self.n_complex * self.free_space_wavenumber |
| 286 | + return PropagationConstantArray(data, coords=self.n_complex.coords) |
| 287 | + |
| 288 | + @property |
| 289 | + def alpha(self) -> AttenuationConstantArray: |
| 290 | + r"""The attenuation constant (real part of :math:`\gamma`). |
| 291 | +
|
| 292 | + Causes exponential decay of the field amplitude: |
| 293 | +
|
| 294 | + .. math:: |
| 295 | +
|
| 296 | + E(z) = E_0 e^{-\alpha z} e^{j\beta z} |
| 297 | +
|
| 298 | + Units: Nepers/meter (Np/m). |
| 299 | + """ |
| 300 | + return -self.gamma.real |
| 301 | + |
| 302 | + @property |
| 303 | + def beta(self) -> PhaseConstantArray: |
| 304 | + r"""The phase constant (imaginary part of :math:`\gamma`). |
| 305 | +
|
| 306 | + Determines the phase variation of the field: |
| 307 | +
|
| 308 | + .. math:: |
| 309 | +
|
| 310 | + E(z) = E_0 e^{-\alpha z} e^{j\beta z} |
| 311 | +
|
| 312 | + Units: radians/meter (rad/m). |
| 313 | + """ |
| 314 | + return self.gamma.imag |
| 315 | + |
| 316 | + @property |
| 317 | + def distance_40dB(self) -> FreqModeDataArray: |
| 318 | + r"""Distance at which the field amplitude drops by 40 dB. |
| 319 | +
|
| 320 | + For a lossy transmission line, this is the distance where the signal |
| 321 | + attenuates by 40 dB: |
| 322 | +
|
| 323 | + .. math:: |
| 324 | +
|
| 325 | + d_{40\text{dB}} = \frac{40\,\text{dB}}{20 \log_{10}(e) \cdot \alpha} = \frac{40}{8.686 \cdot \alpha} |
| 326 | +
|
| 327 | + where :math:`\alpha` is the attenuation constant in Nepers/meter. |
| 328 | +
|
| 329 | + Units: meters. |
| 330 | + """ |
| 331 | + # Convert attenuation from Nepers/m to dB/m: dB/m = 20*log10(e)*Np/m ≈ 8.686*Np/m |
| 332 | + # Then: distance_40dB = 40 dB / (attenuation in dB/m) |
| 333 | + attenuation_dB_per_m = 20 * np.log10(np.e) * self.alpha |
| 334 | + distance_meters = 40 / attenuation_dB_per_m |
| 335 | + return FreqModeDataArray(distance_meters.values, coords=self.alpha.coords) |
| 336 | + |
| 337 | + @property |
| 338 | + def effective_relative_permittivity(self) -> FreqModeDataArray: |
| 339 | + """Effective relative permittivity (real part of n_eff²).""" |
| 340 | + e_r_complex = self.n_complex * self.n_complex |
| 341 | + return FreqModeDataArray(e_r_complex.values, coords=self.n_complex.coords) |
| 342 | + |
| 343 | + @property |
| 344 | + def phase_velocity(self) -> PhaseVelocityArray: |
| 345 | + """Phase velocity (v_p = c/n_eff) in m/s.""" |
| 346 | + C_0_meters = C_0 * 1e-6 |
| 347 | + v_p = C_0_meters / self.n_eff |
| 348 | + return PhaseVelocityArray(v_p.values, coords=self.n_eff.coords) |
| 349 | + |
| 350 | + @property |
| 351 | + def group_velocity(self) -> Optional[GroupVelocityArray]: |
| 352 | + """Group velocity (v_g = c/n_group) in m/s.""" |
| 353 | + if self.n_group_raw is None: |
| 354 | + log.warning( |
| 355 | + "The 'group_velocity' was not computed. To calculate 'group_velocity' index, pass " |
| 356 | + "'group_index_step = True' in the 'MicrowaveModeSpec'.", |
| 357 | + log_once=True, |
| 358 | + ) |
| 359 | + return None |
| 360 | + C_0_meters = C_0 * 1e-6 |
| 361 | + v_g = C_0_meters / self.n_group |
| 362 | + return GroupVelocityArray(v_g.values, coords=self.n_eff.coords) |
| 363 | + |
| 364 | + @property |
| 365 | + def wave_impedance(self) -> ImpedanceFreqModeDataArray: |
| 366 | + r"""Compute the wave impedance associated with the waveguide mode. |
| 367 | + The wave impedance is defined as: |
| 368 | +
|
| 369 | + .. math:: |
| 370 | +
|
| 371 | + Z_{\rm wave} = \frac{\int |E_t|^2 \, {\rm d}S}{2 P}. |
| 372 | + """ |
| 373 | + self._check_fields_stored(["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"]) |
| 374 | + |
| 375 | + tan_fields = self._colocated_tangential_fields |
| 376 | + dim1, dim2 = self._tangential_dims |
| 377 | + e1 = tan_fields["E" + dim1] |
| 378 | + e2 = tan_fields["E" + dim2] |
| 379 | + diff_area = self._diff_area |
| 380 | + field_int = [np.abs(e_field) ** 2 for e_field in [e1, e2]] |
| 381 | + tangential_intensity = (diff_area * (field_int[0] + field_int[1])).sum( |
| 382 | + dim=self._tangential_dims |
| 383 | + ) |
| 384 | + Z_wave = tangential_intensity / self.complex_flux / 2 |
| 385 | + return ImpedanceFreqModeDataArray(Z_wave.values, coords=self.flux.coords) |
| 386 | + |
| 387 | + def _classify_mode(self, mode_index: int) -> ModeClassification: |
| 388 | + """Classify mode as TEM, quasi-TEM, TE, TM, or Hybrid based on TE/TM fractions.""" |
| 389 | + # Make quasi-TEM classification choice based on lowest frequency available |
| 390 | + min_f_idx = self.wg_TE_fraction.f.argmin() |
| 391 | + low_f_TE_frac = self.wg_TE_fraction.sel(mode_index=mode_index).isel(f=min_f_idx).values |
| 392 | + low_f_TM_frac = self.wg_TM_fraction.sel(mode_index=mode_index).isel(f=min_f_idx).values |
| 393 | + # Otherwise we use the average value of the fraction across frequencies |
| 394 | + mean_TE_frac = self.wg_TE_fraction.sel(mode_index=mode_index).mean().values |
| 395 | + mean_TM_frac = self.wg_TM_fraction.sel(mode_index=mode_index).mean().values |
| 396 | + |
| 397 | + if ( |
| 398 | + mean_TE_frac >= self.monitor.mode_spec.tem_polarization_threshold |
| 399 | + and mean_TM_frac >= self.monitor.mode_spec.tem_polarization_threshold |
| 400 | + ): |
| 401 | + return "TEM" |
| 402 | + elif ( |
| 403 | + low_f_TE_frac >= self.monitor.mode_spec.qtem_polarization_threshold |
| 404 | + and low_f_TM_frac >= self.monitor.mode_spec.qtem_polarization_threshold |
| 405 | + ): |
| 406 | + return "quasi-TEM" |
| 407 | + elif mean_TE_frac >= self.monitor.mode_spec.tem_polarization_threshold: |
| 408 | + return "TE" |
| 409 | + elif mean_TM_frac >= self.monitor.mode_spec.tem_polarization_threshold: |
| 410 | + return "TM" |
| 411 | + else: |
| 412 | + return "Hybrid" |
| 413 | + |
245 | 414 | def _group_index_post_process(self, frequency_step: float) -> Self: |
246 | 415 | """Calculate group index and remove added frequencies used only for this calculation. |
247 | 416 |
|
|
0 commit comments