diff --git a/CHANGELOG.md b/CHANGELOG.md index a04d01b741..ef73026738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `smoothed_projection` for topology optimization of completely binarized designs. - Added more RF-specific mode characteristics to `MicrowaveModeData`, including propagation constants (alpha, beta, gamma), phase/group velocities, wave impedance, and automatic mode classification with configurable polarization thresholds in `MicrowaveModeSpec`. - Introduce `tidy3d.rf` namespace to consolidate all RF classes. +- Added `EMEInterfaceSMatrixMonitor` to record the cell-interface s matrices used in the EME solver. ### Breaking Changes - Edge singularity correction at PEC and lossy metal edges defaults to `True`. @@ -71,6 +72,7 @@ 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. ### 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..a56c8196aa 100644 --- a/schemas/EMESimulation.json +++ b/schemas/EMESimulation.json @@ -4718,7 +4718,6 @@ "type": "integer" }, "num_sweep": { - "default": 1, "minimum": 0, "type": "integer" }, @@ -5092,6 +5091,190 @@ ], "type": "object" }, + "EMEInterfaceSMatrixMonitor": { + "additionalProperties": false, + "properties": { + "attrs": { + "default": {}, + "type": "object" + }, + "center": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ], + "default": [ + 0.0, + 0.0, + 0.0 + ] + }, + "colocate": { + "default": true, + "enum": [ + true + ], + "type": "boolean" + }, + "eme_cell_interval_space": { + "default": 1, + "exclusiveMinimum": 0, + "type": "integer" + }, + "freqs": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "ArrayLike" + } + ] + }, + "interval_space": { + "default": [ + 1, + 1, + 1 + ], + "items": [ + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "num_modes": { + "minimum": 0, + "type": "integer" + }, + "num_sweep": { + "minimum": 0, + "type": "integer" + }, + "size": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + "type": { + "default": "EMEInterfaceSMatrixMonitor", + "enum": [ + "EMEInterfaceSMatrixMonitor" + ], + "type": "string" + } + }, + "required": [ + "name", + "size" + ], + "type": "object" + }, "EMELengthSweep": { "additionalProperties": false, "properties": { @@ -12731,6 +12914,7 @@ "mapping": { "EMECoefficientMonitor": "#/definitions/EMECoefficientMonitor", "EMEFieldMonitor": "#/definitions/EMEFieldMonitor", + "EMEInterfaceSMatrixMonitor": "#/definitions/EMEInterfaceSMatrixMonitor", "EMEModeSolverMonitor": "#/definitions/EMEModeSolverMonitor", "MediumMonitor": "#/definitions/MediumMonitor", "ModeSolverMonitor": "#/definitions/ModeSolverMonitor", @@ -12745,6 +12929,9 @@ { "$ref": "#/definitions/EMEFieldMonitor" }, + { + "$ref": "#/definitions/EMEInterfaceSMatrixMonitor" + }, { "$ref": "#/definitions/EMEModeSolverMonitor" }, diff --git a/tests/sims/full_fdtd.h5 b/tests/sims/full_fdtd.h5 index 40730d6e30..ecad9a19f8 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 6712d921b6..c52cd19f5e 100644 --- a/tests/test_components/test_eme.py +++ b/tests/test_components/test_eme.py @@ -54,6 +54,11 @@ def make_eme_sim(): name="coeffs", ) + interface_smatrix_monitor = td.EMEInterfaceSMatrixMonitor( + size=monitor_size, + name="interface_smatrix", + ) + mode_monitor = td.EMEModeSolverMonitor( size=(td.inf, td.inf, td.inf), name="modes", @@ -74,7 +79,14 @@ def make_eme_sim(): name="modes_out", ) - monitors = [mode_monitor, coeff_monitor, field_monitor, modes_in, modes_out] + monitors = [ + mode_monitor, + coeff_monitor, + interface_smatrix_monitor, + field_monitor, + modes_in, + modes_out, + ] structures = [waveguide] sim = td.EMESimulation( @@ -281,6 +293,9 @@ def test_eme_monitor(): _ = td.EMECoefficientMonitor( center=(1, 2, 3), size=(2, 2, 2), freqs=[300e12], num_modes=2, name="eme_coeffs" ) + _ = td.EMEInterfaceSMatrixMonitor( + center=(1, 2, 3), size=(2, 2, 2), freqs=[300e12], num_modes=2, name="eme_interface_smatrix" + ) def test_eme_simulation(): @@ -402,7 +417,7 @@ def test_eme_simulation(): monitor = sim.monitors[0].updated_copy(num_modes=1000) with pytest.raises(SetupError): _ = sim.updated_copy(monitors=[monitor]) - monitor = sim.monitors[2].updated_copy(num_modes=6) + monitor = sim.monitors[3].updated_copy(num_modes=6) with pytest.raises(SetupError): _ = sim.updated_copy(monitors=[monitor]) @@ -419,18 +434,34 @@ 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( + freqs=list(sim.freqs) + list(1e14 * np.linspace(1, 2, 5000)), + grid_spec=sim.grid_spec.updated_copy(wavelength=1), + ) + 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() - large_monitor = sim.monitors[2].updated_copy(size=(td.inf, td.inf, td.inf)) + large_monitor = sim.monitors[3].updated_copy(size=(td.inf, td.inf, td.inf)) _ = sim.updated_copy( size=(10, 10, 10), monitors=[large_monitor], @@ -769,17 +800,63 @@ def _get_eme_coeff_data_array(num_sweep=0): return data +def _get_eme_interface_smatrix_data_array(num_sweep=0): + f = [2e14] + mode_index_out = [0, 1] + mode_index_in = [0, 1, 2] + eme_cell_index = np.arange(6) + if num_sweep != 0: + sweep_index = np.arange(num_sweep) + else: + sweep_index = [0] + coords = { + "f": f, + "sweep_index": sweep_index, + "eme_cell_index": eme_cell_index, + "mode_index_out": mode_index_out, + "mode_index_in": mode_index_in, + } + data = td.EMEInterfaceSMatrixDataArray( + (1 + 1j) + * np.random.random( + ( + len(f), + len(sweep_index), + len(eme_cell_index), + len(mode_index_out), + len(mode_index_in), + ), + ), + coords=coords, + ) + if num_sweep == 0: + data = data.drop_vars("sweep_index") + return data + + def _get_eme_coeff_dataset(num_sweep=0): A = _get_eme_coeff_data_array(num_sweep=num_sweep) B = _get_eme_coeff_data_array(num_sweep=num_sweep) return td.EMECoefficientDataset(A=A, B=B) +def _get_eme_interface_smatrix_dataset(num_sweep=0): + fields = {} + for key in ["S11", "S12", "S21", "S22"]: + fields[key] = _get_eme_interface_smatrix_data_array(num_sweep=num_sweep) + return td.EMEInterfaceSMatrixDataset(**fields) + + def test_eme_coeff_data_array(): _ = _get_eme_coeff_data_array() _ = _get_eme_coeff_data_array(num_sweep=3) +def test_eme_interface_smatrix_data_array(): + _ = _get_eme_interface_smatrix_data_array() + _ = _get_eme_interface_smatrix_data_array(num_sweep=3) + + def _get_eme_mode_index_data_array(num_sweep=0): f = [td.C_0, 3e14] mode_index = np.arange(10) @@ -837,6 +914,9 @@ def test_eme_dataset(): # test coefficient _ = _get_eme_coeff_dataset() + # test interface s matrix + _ = _get_eme_interface_smatrix_dataset() + # test field _ = _get_eme_field_dataset() @@ -905,6 +985,17 @@ def _get_eme_coeff_data(num_sweep=0): return td.EMECoefficientData(monitor=monitor, A=dataset.A, B=dataset.B) +def _get_eme_interface_smatrix_data(num_sweep=0): + dataset = _get_eme_interface_smatrix_dataset(num_sweep=num_sweep) + monitor = td.EMEInterfaceSMatrixMonitor( + size=(td.inf, td.inf, td.inf), + name="interface_smatrix", + ) + return td.EMEInterfaceSMatrixData( + monitor=monitor, S11=dataset.S11, S12=dataset.S12, S21=dataset.S21, S22=dataset.S22 + ) + + def _get_mode_solver_data(modes_out=False, num_modes=3): offset = 1 if modes_out else -1 name = "modes_out" if modes_out else "modes_in" @@ -936,10 +1027,12 @@ def test_eme_monitor_data(): _ = _get_eme_mode_solver_data() _ = _get_eme_field_data() _ = _get_eme_coeff_data() + _ = _get_eme_interface_smatrix_data() _ = _get_mode_solver_data() _ = _get_eme_mode_solver_data(num_sweep=3) _ = _get_eme_field_data(num_sweep=3) _ = _get_eme_coeff_data(num_sweep=3) + _ = _get_eme_interface_smatrix_data(num_sweep=3) def _get_eme_port_modes(num_sweep=0): @@ -960,12 +1053,14 @@ def test_eme_sim_data(): sim = make_eme_sim() mode_monitor_data = _get_eme_mode_solver_data() coeff_monitor_data = _get_eme_coeff_data() + interface_smatrix_monitor_data = _get_eme_interface_smatrix_data() field_monitor_data = _get_eme_field_data() modes_in_data = _get_mode_solver_data(modes_out=False, num_modes=3) modes_out_data = _get_mode_solver_data(modes_out=True, num_modes=2) data = [ mode_monitor_data, coeff_monitor_data, + interface_smatrix_monitor_data, field_monitor_data, modes_in_data, modes_out_data, @@ -1219,7 +1314,7 @@ def test_eme_sim_data(): # test field in basis with freq sweep field_monitor_data = _get_eme_field_data(num_sweep=10) - data[2] = field_monitor_data + data[3] = field_monitor_data sim_data = sim_data.updated_copy(data=data) field_in_basis = sim_data.field_in_basis(field=sim_data["field"], port_index=0) assert len(field_in_basis.Ex.sweep_index) == 10 diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 18f7d99047..30803091ef 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -185,6 +185,7 @@ ChargeDataArray, DiffractionDataArray, EMECoefficientDataArray, + EMEInterfaceSMatrixDataArray, EMEModeIndexDataArray, EMEScalarFieldDataArray, EMEScalarModeFieldDataArray, @@ -243,10 +244,16 @@ from .components.eme.data.dataset import ( EMECoefficientDataset, EMEFieldDataset, + EMEInterfaceSMatrixDataset, EMEModeSolverDataset, EMESMatrixDataset, ) -from .components.eme.data.monitor_data import EMECoefficientData, EMEFieldData, EMEModeSolverData +from .components.eme.data.monitor_data import ( + EMECoefficientData, + EMEFieldData, + EMEInterfaceSMatrixData, + EMEModeSolverData, +) from .components.eme.data.sim_data import EMESimulationData from .components.eme.grid import ( EMECompositeGrid, @@ -258,6 +265,7 @@ from .components.eme.monitor import ( EMECoefficientMonitor, EMEFieldMonitor, + EMEInterfaceSMatrixMonitor, EMEModeSolverMonitor, EMEMonitor, ) @@ -607,6 +615,10 @@ def set_logging_level(level: str) -> None: "EMEFieldMonitor", "EMEFreqSweep", "EMEGrid", + "EMEInterfaceSMatrixData", + "EMEInterfaceSMatrixDataArray", + "EMEInterfaceSMatrixDataset", + "EMEInterfaceSMatrixMonitor", "EMELengthSweep", "EMEModeIndexDataArray", "EMEModeSolverData", diff --git a/tidy3d/components/data/data_array.py b/tidy3d/components/data/data_array.py index ea6385a796..a891cead86 100644 --- a/tidy3d/components/data/data_array.py +++ b/tidy3d/components/data/data_array.py @@ -1176,11 +1176,11 @@ class EMESMatrixDataArray(DataArray): >>> sweep_index = np.arange(10) >>> coords = dict( ... f=f, + ... sweep_index=sweep_index, ... mode_index_out=mode_index_out, ... mode_index_in=mode_index_in, - ... sweep_index=sweep_index ... ) - >>> fd = EMESMatrixDataArray((1 + 1j) * np.random.random((1, 3, 2, 10)), coords=coords) + >>> fd = EMESMatrixDataArray((1 + 1j) * np.random.random((1, 10, 3, 2)), coords=coords) """ __slots__ = () @@ -1188,6 +1188,32 @@ class EMESMatrixDataArray(DataArray): _data_attrs = {"long_name": "scattering matrix element"} +class EMEInterfaceSMatrixDataArray(DataArray): + """Scattering matrix elements at a single cell interface for a fixed pair of ports, + possibly with an extra sweep index. + + Example + ------- + >>> mode_index_in = [0, 1] + >>> mode_index_out = [0, 1, 2] + >>> eme_cell_index = [2, 4] + >>> f = [2e14] + >>> sweep_index = np.arange(10) + >>> coords = dict( + ... f=f, + ... sweep_index=sweep_index, + ... eme_cell_index=eme_cell_index, + ... mode_index_out=mode_index_out, + ... mode_index_in=mode_index_in, + ... ) + >>> fd = EMEInterfaceSMatrixDataArray((1 + 1j) * np.random.random((1, 10, 2, 3, 2)), coords=coords) + """ + + __slots__ = () + _dims = ("f", "sweep_index", "eme_cell_index", "mode_index_out", "mode_index_in") + _data_attrs = {"long_name": "scattering matrix element"} + + class EMEModeIndexDataArray(DataArray): """Complex-valued effective propagation index of an EME mode, also indexed by EME cell. @@ -1596,6 +1622,7 @@ def _make_impedance_data_array(result: DataArray) -> ImpedanceResultType: EMEScalarModeFieldDataArray, EMESMatrixDataArray, EMECoefficientDataArray, + EMEInterfaceSMatrixDataArray, EMEModeIndexDataArray, EMEFreqModeDataArray, ChargeDataArray, diff --git a/tidy3d/components/eme/data/dataset.py b/tidy3d/components/eme/data/dataset.py index 33f45cef2c..57123b25e3 100644 --- a/tidy3d/components/eme/data/dataset.py +++ b/tidy3d/components/eme/data/dataset.py @@ -6,6 +6,7 @@ from tidy3d.components.data.data_array import ( EMECoefficientDataArray, + EMEInterfaceSMatrixDataArray, EMEModeIndexDataArray, EMEScalarFieldDataArray, EMEScalarModeFieldDataArray, @@ -56,6 +57,33 @@ class EMECoefficientDataset(Dataset): ) +class EMEInterfaceSMatrixDataset(Dataset): + """Dataset storing interface s-matrices. + The s-matrix at ``eme_cell_index=i`` is between cells ``i`` and ``i+1``. + """ + + S11: EMEInterfaceSMatrixDataArray = pd.Field( + ..., + title="S11 matrix", + description="S matrix relating output modes at port 1 to input modes at port 1.", + ) + S12: EMEInterfaceSMatrixDataArray = pd.Field( + ..., + title="S12 matrix", + description="S matrix relating output modes at port 1 to input modes at port 2.", + ) + S21: EMEInterfaceSMatrixDataArray = pd.Field( + ..., + title="S21 matrix", + description="S matrix relating output modes at port 2 to input modes at port 1.", + ) + S22: EMEInterfaceSMatrixDataArray = pd.Field( + ..., + title="S22 matrix", + description="S matrix relating output modes at port 2 to input modes at port 2.", + ) + + class EMEFieldDataset(ElectromagneticFieldDataset): """Dataset storing scalar components of E and H fields as a function of freq, mode_index, and port_index.""" diff --git a/tidy3d/components/eme/data/monitor_data.py b/tidy3d/components/eme/data/monitor_data.py index 558023c0e8..138e5db79f 100644 --- a/tidy3d/components/eme/data/monitor_data.py +++ b/tidy3d/components/eme/data/monitor_data.py @@ -16,10 +16,16 @@ from tidy3d.components.eme.monitor import ( EMECoefficientMonitor, EMEFieldMonitor, + EMEInterfaceSMatrixMonitor, EMEModeSolverMonitor, ) -from .dataset import EMECoefficientDataset, EMEFieldDataset, EMEModeSolverDataset +from .dataset import ( + EMECoefficientDataset, + EMEFieldDataset, + EMEInterfaceSMatrixDataset, + EMEModeSolverDataset, +) class EMEModeSolverData(ElectromagneticFieldData, EMEModeSolverDataset): @@ -50,10 +56,21 @@ class EMECoefficientData(AbstractMonitorData, EMECoefficientDataset): ) +class EMEInterfaceSMatrixData(AbstractMonitorData, EMEInterfaceSMatrixDataset): + """Data associated with an EME interface s matrix monitor.""" + + monitor: EMEInterfaceSMatrixMonitor = pd.Field( + ..., + title="EME Interface S Matrix Monitor", + description="EME interface s matrix monitor associated with this data.", + ) + + EMEMonitorDataType = Union[ EMEModeSolverData, EMEFieldData, EMECoefficientData, + EMEInterfaceSMatrixData, ModeSolverData, PermittivityData, MediumData, diff --git a/tidy3d/components/eme/monitor.py b/tidy3d/components/eme/monitor.py index bee336a3c7..114a33aa13 100644 --- a/tidy3d/components/eme/monitor.py +++ b/tidy3d/components/eme/monitor.py @@ -286,6 +286,73 @@ class EMECoefficientMonitor(EMEMonitor): "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, + num_transverse_cells: int, + num_eme_cells: int, + num_freqs: int, + num_modes: int, + num_sweep: int, + ) -> int: + """Size of monitor storage given the number of points after discretization.""" + bytes_single = ( + 4 * BYTES_COMPLEX * num_freqs * num_modes * num_modes * num_eme_cells * num_sweep + ) + return bytes_single + + +class EMEInterfaceSMatrixMonitor(EMEMonitor): + """EME monitor for cell interface s matrices. + + Example + ------- + >>> monitor = EMEInterfaceSMatrixMonitor( + ... center=(1,2,3), + ... size=(2,2,2), + ... freqs=[300e12], + ... num_modes=2, + ... name="eme_interface_smatrix" + ... ) + """ + + interval_space: tuple[Literal[1], Literal[1], Literal[1]] = pd.Field( + (1, 1, 1), + title="Spatial Interval", + description="Number of grid step intervals 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 point of the monitor grid are always included. " + "Not all monitors support values different from 1. Note: This field is not used " + "for 'EMEInterfaceSMatrixMonitor'.", + ) + + 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, " + "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, @@ -306,6 +373,7 @@ def storage_size( EMEModeSolverMonitor, EMEFieldMonitor, EMECoefficientMonitor, + EMEInterfaceSMatrixMonitor, ModeSolverMonitor, PermittivityMonitor, MediumMonitor, diff --git a/tidy3d/components/eme/simulation.py b/tidy3d/components/eme/simulation.py index 6569d58a73..c2e1984010 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"a maximum of {MAX_NUM_SAMPLING_POINTS:.2e} are allowed." + ) + num_freqs = self._num_sampling_points + if num_freqs > MAX_NUM_SAMPLING_POINTS: + raise SetupError( + f"Simulation has {num_freqs:.2e} frequencies, " + f"a maximum of {MAX_NUM_SAMPLING_POINTS:.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"Consider using 'EMEModeSpec.interp_spec' instead for a faster approximate solution." ) - if num_freqs > WARN_NUM_FREQS: + if num_freqs > 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"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) -> pd.NonNegativeFloat: + """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."""