"""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]