Source code for tidy3d.components.eme.sweep

"""Defines sweep settings for the EME simulation."""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Union

from pydantic import Field, PositiveInt, field_validator

from tidy3d.components.base import Tidy3dBaseModel
from tidy3d.components.types import ArrayFloat1D, ArrayInt1D, ArrayLike
from tidy3d.exceptions import SetupError

from .grid import MAX_NUM_REPS


class EMESweepSpec(Tidy3dBaseModel, ABC):
    """Abstract spec for a parameter sweep during the EME propagation step.

    Notes
    -----
        An EME sweep re-runs the propagation step with varied parameters while reusing
        previously computed mode data. This makes sweeps much faster than running
        multiple independent simulations, since the mode solving step is typically the
        most expensive part of an EME simulation.

    See Also
    --------
        :class:`.EMELengthSweep` :
            Sweep over cell lengths.
        :class:`.EMEModeSweep` :
            Sweep over number of modes (convergence testing).
        :class:`.EMEPeriodicitySweep` :
            Sweep over number of periodic repetitions.
        :class:`.EMEFreqSweep` :
            Sweep over frequency using perturbative mode solving.
    """

    @property
    @abstractmethod
    def num_sweep(self) -> PositiveInt:
        """Number of sweep indices."""

    @property
    def sweep_modes(self) -> bool:
        """Whether the sweep changes the modes."""
        return False

    @property
    def sweep_interfaces(self) -> bool:
        """Whether the sweep changes the cell interface scattering matrices."""
        return False

    @property
    def sweep_cells(self) -> bool:
        """Whether the sweep changes the propagation within a cell."""
        return False


[docs] class EMELengthSweep(EMESweepSpec): """Spec for sweeping EME cell lengths. Notes ----- Varies the lengths of EME cells without re-solving modes. This is useful for optimizing device length, since only the propagation phase accumulated within each cell changes. If a 2D array is provided for ``scale_factors``, different cells can be scaled independently at each sweep index. Example ------- >>> sweep_spec = EMELengthSweep(scale_factors=[0.5, 1.0, 1.5, 2.0]) """ scale_factors: ArrayLike = Field( title="Length Scale Factor", description="Length scale factors to be used in the EME propagation step. " "The EME propagation step is repeated after scaling every cell length by this amount. " "The results are stored in 'sim_data.smatrix'. If a 2D array is provided, the " "first index is the sweep index and the second index is the cell index, " "allowing a nonuniform cell scaling along the propagation axis.", ) @property def num_sweep(self) -> PositiveInt: """Number of sweep indices.""" return len(self.scale_factors) @property def sweep_cells(self) -> bool: """Whether the sweep changes the propagation within a cell.""" return True
[docs] class EMEModeSweep(EMESweepSpec): """Spec for sweeping number of modes in EME propagation step. Used for convergence testing. Example ------- >>> sweep_spec = EMEModeSweep(num_modes=[1, 2, 5, 10]) """ num_modes: ArrayInt1D = Field( title="Number of Modes", description="Max number of modes to use in the EME propagation step. " "The EME propagation step is repeated after dropping modes with mode_index " "exceeding this value. This can be used for convergence testing; reliable results " "should be independent of the number of modes used. This value cannot exceed " "the maximum number of modes in any EME cell in the simulation.", ) @property def num_sweep(self) -> PositiveInt: """Number of sweep indices.""" return len(self.num_modes) @property def sweep_interfaces(self) -> bool: """Whether the sweep changes the cell interface scattering matrices.""" return True @property def sweep_cells(self) -> bool: """Whether the sweep changes the propagation within a cell.""" return True
[docs] class EMEFreqSweep(EMESweepSpec): """Spec for sweeping frequency in EME propagation step. Unlike ``sim.freqs``, the frequency sweep is approximate, using a perturbative mode solver relative to the simulation EME modes. This can be a faster way to solve at a larger number of frequencies. Example ------- >>> sweep_spec = EMEFreqSweep(freq_scale_factors=[0.9, 0.95, 1.0, 1.05, 1.1]) """ freq_scale_factors: ArrayFloat1D = Field( title="Frequency Scale Factors", description="Scale factors " "applied to every frequency in 'EMESimulation.freqs'. After applying the scale factors, " "the new modes are computed approximately using the exact modes as a basis. " "If there are multiple 'EMESimulation.freqs', the exact modes are computed at each " "of those frequencies, and then the scale factors are applied to each independently.", ) @property def num_sweep(self) -> PositiveInt: """Number of sweep indices.""" return len(self.freq_scale_factors) @property def sweep_modes(self) -> bool: """Whether the sweep changes the modes.""" return True @property def sweep_interfaces(self) -> bool: """Whether the sweep changes the cell interface scattering matrices.""" return True @property def sweep_cells(self) -> bool: """Whether the sweep changes the propagation within a cell.""" return True
[docs] class EMEPeriodicitySweep(EMESweepSpec): """Spec for sweeping number of repetitions of EME subgrids. Notes ----- Useful for simulating long periodic structures like Bragg gratings, as it allows the EME solver to reuse the modes and cell interface scattering matrices. Compared to setting ``num_reps`` directly in the ``eme_grid_spec``, this sweep spec allows varying the number of repetitions, effectively simulating multiple structures in a single EME simulation. Example ------- >>> n_list = [1, 50, 100] >>> sweep_spec = EMEPeriodicitySweep(num_reps=[{"unit_cell": n} for n in n_list]) """ num_reps: list[dict[str, PositiveInt]] = Field( title="Number of Repetitions", description="Number of periodic repetitions of named subgrids in this EME grid. " "At each sweep index, contains a dict mapping the name of a subgrid to the " "number of repetitions of that subgrid at that sweep index.", ) @field_validator("num_reps") @classmethod def _validate_num_reps(cls, val: list[dict[str, PositiveInt]]) -> list[dict[str, PositiveInt]]: """Check num_reps is not too large.""" for num_reps_dict in val: for value in num_reps_dict.values(): if value > MAX_NUM_REPS: raise SetupError( f"'EMEGridSpec' has 'num_reps={value:.2e}'; " f"the largest value allowed is '{MAX_NUM_REPS}'." ) return val @property def num_sweep(self) -> PositiveInt: """Number of sweep indices.""" return len(self.num_reps)
EMESweepSpecType = Union[EMELengthSweep, EMEModeSweep, EMEFreqSweep, EMEPeriodicitySweep]