diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a791d11a..54338d0034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Port names in `ModalComponentModeler` and `TerminalComponentModeler` can no longer include the `@` symbol. - Improved speed of convolutions for large inputs. - Default value of `EMEModeSpec.interp_spec` is `ModeInterpSpec.cheb(num_points=3, reduce_data=True)` for faster multi-frequency EME simulations. +- Default value of `num_sweep` in `EMECoefficientMonitor` is now `None`, recording all sweep indices. +- Maximum number of frequencies in an `EMESimulation` is now larger if mode interpolation via `ModeInterpSpec` is used. ### Fixed - Ensured the legacy `Env` proxy mirrors `config.web` profile switches and preserves API URL. diff --git a/schemas/EMESimulation.json b/schemas/EMESimulation.json index 2891f92347..496a9174fb 100644 --- a/schemas/EMESimulation.json +++ b/schemas/EMESimulation.json @@ -4718,7 +4718,6 @@ "type": "integer" }, "num_sweep": { - "default": 1, "minimum": 0, "type": "integer" }, diff --git a/tests/sims/full_fdtd.h5 b/tests/sims/full_fdtd.h5 index 40730d6e30..cd3de0886d 100644 Binary files a/tests/sims/full_fdtd.h5 and b/tests/sims/full_fdtd.h5 differ diff --git a/tests/sims/full_fdtd.json b/tests/sims/full_fdtd.json index e048a146d1..a03541827f 100644 --- a/tests/sims/full_fdtd.json +++ b/tests/sims/full_fdtd.json @@ -140,6 +140,7 @@ "type": "CustomMedium", "interp_method": "nearest", "subpixel": false, + "derived_from": null, "eps_dataset": null, "permittivity": "SpatialDataArray", "conductivity": null @@ -988,6 +989,7 @@ "type": "CustomMedium", "interp_method": "nearest", "subpixel": false, + "derived_from": null, "eps_dataset": null, "permittivity": "SpatialDataArray", "conductivity": null @@ -1032,7 +1034,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { @@ -1075,7 +1078,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { @@ -1117,7 +1121,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { @@ -1159,7 +1164,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { @@ -1200,7 +1206,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { @@ -1236,6 +1243,7 @@ "type": "CustomMedium", "interp_method": "nearest", "subpixel": false, + "derived_from": null, "eps_dataset": null, "permittivity": { "attrs": {}, @@ -1304,7 +1312,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { @@ -1371,7 +1380,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { @@ -1431,7 +1441,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { @@ -1491,7 +1502,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { @@ -1544,7 +1556,8 @@ ] ], "interp_method": "nearest", - "subpixel": false + "subpixel": false, + "derived_from": null } }, { diff --git a/tests/test_components/test_eme.py b/tests/test_components/test_eme.py index 8e61a5cbd7..058ae384ac 100644 --- a/tests/test_components/test_eme.py +++ b/tests/test_components/test_eme.py @@ -419,14 +419,29 @@ def test_eme_simulation(): with AssertLogLevel("WARNING", "slow-down"): sim_bad.validate_pre_upload() - sim_bad = sim.updated_copy( + sim_ok = sim.updated_copy( freqs=list(sim.freqs) + list(1e14 * np.linspace(1, 2, 1000)), grid_spec=sim.grid_spec.updated_copy(wavelength=1), ) + sim_ok.validate_pre_upload() + eme_grid_spec_no_interp = td.EMECompositeGrid( + subgrids=[ + s.updated_copy(interp_spec=None, path="mode_spec") + for s in sim_ok.eme_grid_spec.subgrids + ], + subgrid_boundaries=[-1, 1], + ) + sim_bad = sim_ok.updated_copy(eme_grid_spec=eme_grid_spec_no_interp) with pytest.raises(SetupError): sim_bad.validate_pre_upload() - sim_bad = sim.updated_copy( + sim_bad = sim_ok.updated_copy( + freqs=list(sim.freqs) + list(1e14 * np.linspace(1, 2, 5000)), + ) + with pytest.raises(SetupError): + sim_bad.validate_pre_upload() + sim_bad = sim_ok.updated_copy( freqs=list(sim.freqs) + list(1e14 * np.linspace(1, 2, 100)), + eme_grid_spec=eme_grid_spec_no_interp, ) with AssertLogLevel("WARNING", contains_str="expensive"): sim_bad.validate_pre_upload() diff --git a/tidy3d/components/eme/monitor.py b/tidy3d/components/eme/monitor.py index bee336a3c7..ebbae89a26 100644 --- a/tidy3d/components/eme/monitor.py +++ b/tidy3d/components/eme/monitor.py @@ -59,7 +59,7 @@ class EMEMonitor(AbstractMonitor, ABC): eme_cell_interval_space: Literal[1] = pd.Field( 1, title="EME Cell Interval", - description="Number of eme cells between monitor recordings. If equal to 1, " + description="Number of EME cells between monitor recordings. If equal to 1, " "there will be no downsampling. If greater than 1, the step will be applied, but the " "first and last cells are always included. Not used in all monitors. " "Not all monitors support values different from 1.", @@ -141,7 +141,7 @@ class EMEModeSolverMonitor(EMEMonitor): eme_cell_interval_space: pd.PositiveInt = pd.Field( 1, title="EME Cell Interval", - description="Number of eme cells between monitor recordings. If equal to 1, " + description="Number of EME cells between monitor recordings. If equal to 1, " "there will be no downsampling. If greater than 1, the step will be applied, but the " "first and last cells are always included. Not used in all monitors. " "Not all monitors support values different from 1.", @@ -214,7 +214,7 @@ class EMEFieldMonitor(EMEMonitor, AbstractFieldMonitor): eme_cell_interval_space: Literal[1] = pd.Field( 1, title="EME Cell Interval", - description="Number of eme cells between monitor recordings. If equal to 1, " + description="Number of EME cells between monitor recordings. If equal to 1, " "there will be no downsampling. If greater than 1, the step will be applied, but the " "first and last cells are always included. Not used in all monitors. " "Not all monitors support values different from 1. Note: this field is not used for " @@ -280,12 +280,21 @@ class EMECoefficientMonitor(EMEMonitor): eme_cell_interval_space: pd.PositiveInt = pd.Field( 1, title="EME Cell Interval", - description="Number of eme cells between monitor recordings. If equal to 1, " + description="Number of EME cells between monitor recordings. If equal to 1, " "there will be no downsampling. If greater than 1, the step will be applied, but the " "first and last cells are always included. Not used in all monitors. " "Not all monitors support values different from 1.", ) + num_sweep: Optional[pd.NonNegativeInt] = pd.Field( + None, + title="Number of Sweep Indices", + description="Number of sweep indices for the monitor to record. " + "Cannot exceed the number of sweep indices for the simulation. " + "If the sweep does not change the monitor data, the sweep index " + "will be omitted. A value of 'None' will record all sweep indices.", + ) + def storage_size( self, num_cells: int, diff --git a/tidy3d/components/eme/simulation.py b/tidy3d/components/eme/simulation.py index 6569d58a73..9f9c0841c7 100644 --- a/tidy3d/components/eme/simulation.py +++ b/tidy3d/components/eme/simulation.py @@ -51,8 +51,9 @@ # eme specific simulation parameters -WARN_NUM_FREQS = 20 -MAX_NUM_FREQS = 500 +WARN_NUM_SAMPLING_POINTS = 20 +MAX_NUM_SAMPLING_POINTS = 500 +MAX_NUM_FREQS = 2000 MAX_NUM_SWEEP = 100 @@ -105,7 +106,7 @@ class EMESimulation(AbstractYeeGridSimulation): **Frequency Sweeps** - Frequency sweeps are supported by including multiple frequencies in the `freqs` field. However, our EME solver repeats the mode solving for each new frequency, so frequency sweeps involving a large number of frequencies can be slow and expensive. If a large number of frequencies are required, consider using our FDTD solver instead. + Frequency sweeps are supported by including multiple frequencies in the `freqs` field. To avoid recomputing the modes at each frequency, the modes are interpolated according to the `EMEModeSpec.interp_spec` in the cells `eme_grid_spec`. By setting this `interp_spec`, the interpolation can be changed or disabled (repeating the solve at each frequency, which can be slow). **Passivity and Unitarity Constraints** @@ -159,10 +160,9 @@ class EMESimulation(AbstractYeeGridSimulation): ..., title="Frequencies", description="Frequencies for the EME simulation. " - "The field is propagated independently at each provided frequency. " - "This can be slow when the number of frequencies is large. In this case, " - "consider using the approximate 'EMEFreqSweep' as the 'sweep_spec' " - "instead of providing all desired frequencies here.", + "The field is propagated independently at each provided frequency, " + "but the modes are only computed at a few sampling points and interpolated. " + "To change this behavior, you can use 'EMEModeSpec.interp_spec'.", ) axis: Axis = pd.Field( @@ -817,17 +817,23 @@ def _validate_size(self) -> None: if num_freqs > MAX_NUM_FREQS: raise SetupError( f"Simulation has {num_freqs:.2e} frequencies, " - f"a maximum of {MAX_NUM_FREQS:.2e} are allowed. Mode solving " - f"is repeated at each frequency, so EME simulations with too many frequencies " - f"can be slower and more expensive than FDTD simulations. " - f"Consider using an 'EMEFreqSweep' instead for a faster approximate solution." + f"a maximum of {MAX_NUM_FREQS:.2e} are allowed." ) - if num_freqs > WARN_NUM_FREQS: + num_sampling_points = self._num_sampling_points + if num_sampling_points > MAX_NUM_SAMPLING_POINTS: + raise SetupError( + f"Simulation has {num_sampling_points:.2e} frequency sampling points, " + f"a maximum of {MAX_NUM_SAMPLING_POINTS:.2e} are allowed. Mode solving " + f"is repeated at each sampling point, so EME simulations with too many " + f"frequencies can be slower and more expensive than FDTD simulations. " + f"Consider using 'EMEModeSpec.interp_spec' instead for a faster approximate solution." + ) + if num_sampling_points > WARN_NUM_SAMPLING_POINTS: log.warning( - f"Simulation has {num_freqs:.2e} frequencies. Mode solving " - f"is repeated at each frequency, so EME simulations with too many frequencies " - f"can be slower and more expensive than FDTD simulations. " - f"Consider using an 'EMEFreqSweep' instead for a faster approximate solution." + f"Simulation has {num_sampling_points:.2e} frequency sampling points. Mode solving " + f"is repeated at each sampling point, so EME simulations with too many " + f"frequencies can be slower and more expensive than FDTD simulations. " + f"Consider using 'EMEModeSpec.interp_spec' instead for a faster approximate solution." ) def _validate_monitor_size(self) -> None: @@ -934,6 +940,18 @@ def monitors_data_size(self) -> dict[str, float]: data_size[monitor.name] = storage_size return data_size + @property + def _num_sampling_points(self) -> int: + """Max number of sampling freqs in the simulation.""" + freqs = set() + for mode_spec in self.eme_grid.mode_specs: + interp_spec = mode_spec.interp_spec + if interp_spec is None: + freqs |= set(self.freqs) + else: + freqs |= set(interp_spec.sampling_points(self.freqs)) + return len(freqs) + @property def _num_sweep(self) -> pd.PositiveInt: """Number of sweep indices."""