Source code for tidy3d.components.eme.data.dataset

"""EME dataset"""

from __future__ import annotations

import numpy as np
from pydantic import Field

from tidy3d.components.base import cached_property
from tidy3d.components.data.data_array import (
    EMECoefficientDataArray,
    EMEFluxDataArray,
    EMEInterfaceCellIndexDataArray,
    EMEInterfaceDiagnosticDataArray,
    EMEInterfaceSMatrixDataArray,
    EMEModeIndexDataArray,
    EMEScalarFieldDataArray,
    EMEScalarModeFieldDataArray,
    EMESMatrixDataArray,
)
from tidy3d.components.data.dataset import Dataset, ElectromagneticFieldDataset
from tidy3d.exceptions import ValidationError


[docs] class EMESMatrixDataset(Dataset): """Dataset storing the scattering matrix of an EME simulation. Notes ----- The S matrix relates incoming and outgoing mode amplitudes at the two ports of the EME device. Port 1 is at the beginning and port 2 is at the end of the propagation axis. Convention: - ``S11``: reflection at port 1 (input at port 1, output at port 1). - ``S21``: transmission from port 1 to port 2. - ``S12``: transmission from port 2 to port 1. - ``S22``: reflection at port 2 (input at port 2, output at port 2). Each element is indexed by ``(f, mode_index_out, mode_index_in)`` and optionally ``sweep_index`` when a sweep is used. See Also -------- :class:`.EMESimulationData` : Simulation data containing the S matrix. Example ------- >>> from tidy3d import EMESMatrixDataArray >>> f = [2e14] >>> sweep_index = [0] >>> mode_index_in = [0, 1] >>> mode_index_out = [0, 1] >>> coords = dict( ... f=f, sweep_index=sweep_index, ... mode_index_out=mode_index_out, mode_index_in=mode_index_in, ... ) >>> S = EMESMatrixDataArray((1+1j) * np.random.random((1, 1, 2, 2)), coords=coords) >>> smatrix = EMESMatrixDataset(S11=S, S12=S, S21=S, S22=S) """ S11: EMESMatrixDataArray = Field( title="S11 matrix", description="S matrix relating output modes at port 1 to input modes at port 1.", ) S12: EMESMatrixDataArray = Field( title="S12 matrix", description="S matrix relating output modes at port 1 to input modes at port 2.", ) S21: EMESMatrixDataArray = Field( title="S21 matrix", description="S matrix relating output modes at port 2 to input modes at port 1.", ) S22: EMESMatrixDataArray = Field( title="S22 matrix", description="S matrix relating output modes at port 2 to input modes at port 2.", )
class EMEInterfaceSMatrixDataset(Dataset): """Dataset storing S matrices associated with EME cell interfaces.""" S11: EMEInterfaceSMatrixDataArray = Field( title="S11 matrix", description="S matrix relating output modes at port 1 to input modes at port 1.", ) S12: EMEInterfaceSMatrixDataArray = Field( title="S12 matrix", description="S matrix relating output modes at port 1 to input modes at port 2.", ) S21: EMEInterfaceSMatrixDataArray = Field( title="S21 matrix", description="S matrix relating output modes at port 2 to input modes at port 1.", ) S22: EMEInterfaceSMatrixDataArray = Field( title="S22 matrix", description="S matrix relating output modes at port 2 to input modes at port 2.", )
[docs] class EMEOverlapDataset(Dataset): """Dataset storing overlaps between EME modes. Notes ----- ``Oij`` is the unconjugated overlap computed using the E field of cell ``i`` and the H field of cell ``j``. For consistency with ``Sij``, ``mode_index_out`` refers to the mode index in cell ``i``, and ``mode_index_in`` refers to the mode index in cell ``j``. """ O11: EMEInterfaceSMatrixDataArray = Field( title="O11 matrix", description="Overlap integral between E field and H field in the same cell.", ) O12: EMEInterfaceSMatrixDataArray = Field( title="O12 matrix", description="Overlap integral between E field on side 1 and H field on side 2.", ) O21: EMEInterfaceSMatrixDataArray = Field( title="O21 matrix", description="Overlap integral between E field on side 2 and H field on side 1.", )
[docs] class EMECoefficientDataset(Dataset): """Dataset storing various coefficients related to the EME simulation. Notes ----- These coefficients can be used for debugging or optimization. The ``A`` and ``B`` fields store the expansion coefficients for the modes in a cell. These are defined at the cell centers. The ``n_complex`` and ``flux`` fields respectively store the complex-valued effective propagation index and the power flux associated with the modes used in the EME calculation. The ``interface_Sij`` fields store the S matrices associated with the interfaces between EME cells. Example ------- >>> from tidy3d import EMECoefficientDataArray >>> f = [2e14] >>> sweep_index = [0] >>> eme_port_index = [0, 1] >>> eme_cell_index = [0, 1, 2] >>> mode_index_out = [0, 1] >>> mode_index_in = [0, 1] >>> coords = dict( ... f=f, sweep_index=sweep_index, eme_port_index=eme_port_index, ... eme_cell_index=eme_cell_index, ... mode_index_out=mode_index_out, mode_index_in=mode_index_in, ... ) >>> A = EMECoefficientDataArray((1+1j) * np.random.random((1, 1, 2, 3, 2, 2)), coords=coords) >>> data = EMECoefficientDataset(A=A, B=A) """ A: EMECoefficientDataArray | None = Field( None, title="A coefficient", description="Coefficient for forward mode in this cell.", ) B: EMECoefficientDataArray | None = Field( None, title="B coefficient", description="Coefficient for backward mode in this cell.", ) n_complex: EMEModeIndexDataArray | None = Field( None, title="Propagation Index", description="Complex-valued effective propagation indices associated with the EME modes.", ) flux: EMEFluxDataArray | None = Field( None, title="Flux", description="Power flux of the EME modes.", ) interface_smatrices: EMEInterfaceSMatrixDataset | None = Field( None, title="Interface S Matrices", description="S matrices associated with the interfaces between EME cells.", ) overlaps: EMEOverlapDataset | None = Field( None, title="Overlaps", description="Overlaps between EME modes." ) @cached_property def normalized_copy(self) -> EMECoefficientDataset: """Return a flux-normalized copy of this dataset. The ``A`` and ``B`` coefficients as well as all four ``interface_smatrices`` blocks are normalized using the square root of the absolute real mode flux, so that the forward and backward power of each mode are given directly by ``|A|^2`` and ``|B|^2``, respectively, without additional normalization by flux. Zero-flux modes are left unscaled. Interface S-matrix normalization requires flux for both adjacent EME cells. Returns ------- :class:`.EMECoefficientDataset` A new dataset with flux-normalized coefficients. The ``flux`` field is set to ``None`` to prevent double normalization. """ if self.flux is None: raise ValidationError( "The 'flux' field of the 'EMECoefficientDataset' is 'None', " "so normalization cannot be performed." ) fields = {"A": self.A, "B": self.B} power_flux = np.abs(np.real(self.flux)) scale = np.sqrt(power_flux) scale = scale.where(scale != 0, 1) flux_cell_indices = np.asarray(scale.eme_cell_index.values) for key, field in fields.items(): if field is not None: field_cell_indices = np.asarray(field.eme_cell_index.values) missing_cell_indices = np.setdiff1d(field_cell_indices, flux_cell_indices) if len(missing_cell_indices) > 0: missing = ", ".join(str(cell_index) for cell_index in missing_cell_indices) raise ValidationError( f"Cannot flux-normalize {key} because the 'flux' field is missing " f"EME cell indices used by {key}: {missing}. For repeated-grid " "EMECoefficientMonitor data, 'normalized_copy' cannot remap " "physical-cell flux onto virtual-cell A/B rows without the " "simulation's virtual-cell mapping; manually expand/remap flux to " "the same 'eme_cell_index' coordinates before normalizing." ) scale_field = scale.sel(eme_cell_index=field_cell_indices) fields[key] = field * scale_field.rename(mode_index="mode_index_out") if self.interface_smatrices is not None: interface_smatrices = self.interface_smatrices interface_cell_indices = np.asarray(interface_smatrices.S12.eme_cell_index.values) for key, smatrix in { "S11": interface_smatrices.S11, "S21": interface_smatrices.S21, "S22": interface_smatrices.S22, }.items(): if not np.array_equal(smatrix.eme_cell_index.values, interface_cell_indices): raise ValidationError( "Cannot flux-normalize interface_smatrices because all S-matrix " "blocks must have identical 'eme_cell_index' coordinates; " f"{key} differs from S12." ) right_cell_indices = interface_cell_indices + 1 required_cell_indices = np.union1d(interface_cell_indices, right_cell_indices) missing_cell_indices = np.setdiff1d(required_cell_indices, flux_cell_indices) if len(missing_cell_indices) > 0: missing = ", ".join(str(cell_index) for cell_index in missing_cell_indices) raise ValidationError( "Cannot flux-normalize interface_smatrices because the 'flux' field is " f"missing EME cell indices required by the interfaces: {missing}. " "For downsampled EMECoefficientMonitor data, use " "'eme_cell_interval_space=1' when requesting normalized interface " "S matrices, or provide flux for both cells adjacent to each interface." ) scale1 = scale.sel(eme_cell_index=interface_cell_indices) scale2 = scale.sel(eme_cell_index=right_cell_indices) scale2 = scale2.assign_coords(eme_cell_index=interface_cell_indices) scale1_out = scale1.rename(mode_index="mode_index_out") scale1_in = scale1.rename(mode_index="mode_index_in") scale2_out = scale2.rename(mode_index="mode_index_out") scale2_in = scale2.rename(mode_index="mode_index_in") fields["interface_smatrices"] = interface_smatrices.updated_copy( S11=interface_smatrices.S11 * scale1_out / scale1_in, S12=interface_smatrices.S12 * scale1_out / scale2_in, S21=interface_smatrices.S21 * scale2_out / scale1_in, S22=interface_smatrices.S22 * scale2_out / scale2_in, ) # for safety to prevent normalizing twice fields["flux"] = None return self.updated_copy(**fields)
class EMEInterfaceDiagnostics(Dataset): """Direct physical residual diagnostics for EME interface solves. Each residual array is indexed by ``(f, sweep_index, eme_interface_index, eme_port_index, mode_index)``. The :attr:`cell_index` and :attr:`right_cell_index` arrays map each ``eme_interface_index`` to the corresponding left/right EME cell pair, which keeps periodicity-sweep interfaces uniquely addressable even when multiple interfaces share the same left cell. The two tangential-field residuals are incident-normalized squared field-energy ratios, so they are dimensionless and directly comparable; the L2-balanced sum is available via the :attr:`normalized_tangential_residual` property. Aperture variants exclude mode-solver PML cells from the field metric and are available via :attr:`normalized_aperture_tangential_residual`. """ cell_index: EMEInterfaceCellIndexDataArray = Field( title="Left Cell Index", description="Left EME cell index for each interface diagnostic row.", ) right_cell_index: EMEInterfaceCellIndexDataArray = Field( title="Right Cell Index", description="Right EME cell index for each interface diagnostic row.", ) normalized_tangential_E_residual: EMEInterfaceDiagnosticDataArray = Field( title="Normalized Tangential E Residual", description=( "Tangential electric-field residual energy divided by the fixed incident " "tangential-field energy at EME interfaces." ), ) normalized_tangential_H_residual: EMEInterfaceDiagnosticDataArray = Field( title="Normalized Tangential H Residual", description=( "Impedance-scaled tangential magnetic-field residual energy divided by the fixed " "incident tangential-field energy at EME interfaces." ), ) normalized_aperture_tangential_E_residual: EMEInterfaceDiagnosticDataArray = Field( title="Normalized Aperture Tangential E Residual", description=( "Tangential electric-field residual energy over the non-PML aperture divided by the " "fixed incident tangential-field energy over the same aperture at EME interfaces." ), ) normalized_aperture_tangential_H_residual: EMEInterfaceDiagnosticDataArray = Field( title="Normalized Aperture Tangential H Residual", description=( "Impedance-scaled tangential magnetic-field residual energy over the non-PML aperture " "divided by the fixed incident tangential-field energy over the same aperture at EME " "interfaces." ), ) power_defect: EMEInterfaceDiagnosticDataArray = Field( title="Power Defect", description=( "Absolute interface power conservation defect for each incident mode; NaN when " "the incident real power is negligible." ), ) @cached_property def normalized_tangential_residual(self) -> EMEInterfaceDiagnosticDataArray: """L2-balanced sum of the normalized squared E and H tangential-field residuals.""" return self.normalized_tangential_E_residual + self.normalized_tangential_H_residual @cached_property def normalized_aperture_tangential_residual(self) -> EMEInterfaceDiagnosticDataArray: """Non-PML-aperture sum of the normalized squared E and H residuals.""" return ( self.normalized_aperture_tangential_E_residual + self.normalized_aperture_tangential_H_residual ) class EMEDiagnosticsData(Dataset): """Diagnostic quantities associated with an EME simulation.""" interface_residuals: EMEInterfaceDiagnostics | None = Field( None, title="Interface Residuals", description="Direct physical residual diagnostics for local EME interface solves.", )
[docs] class EMEFieldDataset(ElectromagneticFieldDataset): """Dataset storing scalar components of E and H fields from EME propagation. Notes ----- Each field component is an :class:`.EMEScalarFieldDataArray` with coordinates ``(x, y, z, f, sweep_index, eme_port_index, mode_index)``, where ``eme_port_index`` indicates which port is excited and ``mode_index`` indicates which mode at that port is excited. Example ------- >>> from tidy3d import EMEScalarFieldDataArray >>> x = [0, 1] >>> y = [0, 1, 2] >>> z = [0] >>> f = [2e14] >>> sweep_index = [0] >>> eme_port_index = [0, 1] >>> mode_index = [0, 1] >>> coords = dict( ... x=x, y=y, z=z, f=f, sweep_index=sweep_index, ... eme_port_index=eme_port_index, mode_index=mode_index, ... ) >>> field = EMEScalarFieldDataArray((1+1j) * np.random.random((2,3,1,1,1,2,2)), coords=coords) >>> data = EMEFieldDataset(Ex=field, Hy=field) """ Ex: EMEScalarFieldDataArray | None = Field( None, title="Ex", description="Spatial distribution of the x-component of the electric field of the mode.", ) Ey: EMEScalarFieldDataArray | None = Field( None, title="Ey", description="Spatial distribution of the y-component of the electric field of the mode.", ) Ez: EMEScalarFieldDataArray | None = Field( None, title="Ez", description="Spatial distribution of the z-component of the electric field of the mode.", ) Hx: EMEScalarFieldDataArray | None = Field( None, title="Hx", description="Spatial distribution of the x-component of the magnetic field of the mode.", ) Hy: EMEScalarFieldDataArray | None = Field( None, title="Hy", description="Spatial distribution of the y-component of the magnetic field of the mode.", ) Hz: EMEScalarFieldDataArray | None = Field( None, title="Hz", description="Spatial distribution of the z-component of the magnetic field of the mode.", )
[docs] class EMEModeSolverDataset(ElectromagneticFieldDataset): """Dataset storing the eigenmodes computed at each EME cell. Notes ----- Each field component is an :class:`.EMEScalarModeFieldDataArray` with coordinates ``(x, y, z, f, eme_cell_index, mode_index)`` and optionally ``sweep_index`` when a frequency sweep is used. Also stores the complex propagation index ``n_complex`` for each mode. Example ------- >>> from tidy3d import EMEScalarModeFieldDataArray, EMEModeIndexDataArray >>> x = [0, 1] >>> y = [0, 1, 2] >>> z = [0] >>> f = [2e14] >>> sweep_index = [0] >>> eme_cell_index = [0, 1, 2] >>> mode_index = [0, 1] >>> field_coords = dict( ... x=x, y=y, z=z, f=f, sweep_index=sweep_index, ... eme_cell_index=eme_cell_index, mode_index=mode_index, ... ) >>> field = EMEScalarModeFieldDataArray( ... (1+1j) * np.random.random((2,3,1,1,1,3,2)), coords=field_coords ... ) >>> index_coords = dict( ... f=f, sweep_index=sweep_index, eme_cell_index=eme_cell_index, mode_index=mode_index, ... ) >>> n_complex = EMEModeIndexDataArray((1+0.01j) * np.ones((1,1,3,2)), coords=index_coords) >>> data = EMEModeSolverDataset( ... n_complex=n_complex, Ex=field, Ey=field, Ez=field, Hx=field, Hy=field, Hz=field, ... ) """ n_complex: EMEModeIndexDataArray = Field( title="Propagation Index", description="Complex-valued effective propagation indices associated with the mode.", ) Ex: EMEScalarModeFieldDataArray = Field( title="Ex", description="Spatial distribution of the x-component of the electric field of the mode.", ) Ey: EMEScalarModeFieldDataArray = Field( title="Ey", description="Spatial distribution of the y-component of the electric field of the mode.", ) Ez: EMEScalarModeFieldDataArray = Field( title="Ez", description="Spatial distribution of the z-component of the electric field of the mode.", ) Hx: EMEScalarModeFieldDataArray = Field( title="Hx", description="Spatial distribution of the x-component of the magnetic field of the mode.", ) Hy: EMEScalarModeFieldDataArray = Field( title="Hy", description="Spatial distribution of the y-component of the magnetic field of the mode.", ) Hz: EMEScalarModeFieldDataArray = Field( title="Hz", description="Spatial distribution of the z-component of the magnetic field of the mode.", )