Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added current integral specification classes: `AxisAlignedCurrentIntegralSpec`, `CompositeCurrentIntegralSpec`, and `Custom2DCurrentIntegralSpec`.
- `sort_spec` in `ModeSpec` allows for fine-grained filtering and sorting of modes. This also deprecates `filter_pol`. The equivalent usage for example to `filter_pol="te"` is `sort_spec=ModeSortSpec(filter_key="TE_polarization", filter_reference=0.5)`. `ModeSpec.track_freq` has also been deprecated and moved to `ModeSortSpec.track_freq`.
- Added `custom_source_time` parameter to `ComponentModeler` classes (`ModalComponentModeler` and `TerminalComponentModeler`), allowing specification of custom source time dependence.
- Validation for `run_only` field in component modelers to catch duplicate or invalid matrix indices early with clear error messages.

### Changed
- Improved performance of antenna metrics calculation by utilizing cached wave amplitude calculations instead of recomputing wave amplitudes for each port excitation in the `TerminalComponentModelerData`.
Expand Down
28 changes: 28 additions & 0 deletions tests/test_plugins/smatrix/test_component_modeler.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,31 @@ def test_custom_source_time(monkeypatch):
):
custom_source = td.GaussianPulse(freq0=td.C_0, fwidth=1e12)
modeler = make_component_modeler(custom_source_time=custom_source)


def test_validate_run_only_uniqueness_modal():
"""Test that run_only validator rejects duplicate entries for ModalComponentModeler."""
modeler = make_component_modeler()

# Get valid matrix indices (port_name, mode_index)
port0_idx = (modeler.ports[0].name, 0)
port1_idx = (modeler.ports[1].name, 0)

# Test with duplicate entries - should raise ValidationError
with pytest.raises(pydantic.ValidationError, match="duplicate entries"):
modeler.updated_copy(run_only=(port0_idx, port0_idx, port1_idx))


def test_validate_run_only_membership_modal():
"""Test that run_only validator rejects invalid indices for ModalComponentModeler."""
modeler = make_component_modeler()

# Test with invalid port name
with pytest.raises(pydantic.ValidationError, match="not present in"):
modeler.updated_copy(run_only=(("invalid_port", 0),))

# Test with invalid mode index
port0_name = modeler.ports[0].name
invalid_mode = modeler.ports[0].mode_spec.num_modes + 1
with pytest.raises(pydantic.ValidationError, match="not present in"):
modeler.updated_copy(run_only=((port0_name, invalid_mode),))
46 changes: 46 additions & 0 deletions tests/test_plugins/smatrix/test_terminal_component_modeler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1804,3 +1804,49 @@ def test_custom_source_time(monkeypatch, tmp_path):
for source in sim.sources:
assert source.source_time.freq0 == custom_source.freq0
assert source.source_time.fwidth == custom_source.fwidth


def test_validate_run_only_uniqueness():
"""Test that run_only validator rejects duplicate entries for TerminalComponentModeler."""
modeler = make_component_modeler(planar_pec=True)

# Get valid network indices
port0_idx = modeler.network_index(modeler.ports[0])
port1_idx = modeler.network_index(modeler.ports[1])

# Test with duplicate entries - should raise ValidationError
with pytest.raises(pd.ValidationError, match="duplicate entries"):
modeler.updated_copy(run_only=(port0_idx, port0_idx, port1_idx))


def test_validate_run_only_membership():
"""Test that run_only validator rejects invalid indices for TerminalComponentModeler."""
modeler = make_component_modeler(planar_pec=True)

# Test with invalid index - should raise ValidationError
with pytest.raises(pd.ValidationError, match="not present in"):
modeler.updated_copy(run_only=("invalid_port_name",))

# Test with partially invalid indices
port0_idx = modeler.network_index(modeler.ports[0])
with pytest.raises(pd.ValidationError, match="not present in"):
modeler.updated_copy(run_only=(port0_idx, "invalid_port"))


def test_validate_run_only_with_wave_ports():
"""Test run_only validation with WavePorts in TerminalComponentModeler."""
z_grid = td.UniformGrid(dl=1 * 1e3)
xy_grid = td.UniformGrid(dl=0.1 * 1e3)
grid_spec = td.GridSpec(grid_x=xy_grid, grid_y=xy_grid, grid_z=z_grid)
modeler = make_coaxial_component_modeler(port_types=(WavePort, WavePort), grid_spec=grid_spec)

port0_idx = modeler.network_index(modeler.ports[0])
port1_idx = modeler.network_index(modeler.ports[1])

# Valid case
modeler_updated = modeler.updated_copy(run_only=(port0_idx,))
assert modeler_updated.run_only == (port0_idx,)

# Invalid case
with pytest.raises(pd.ValidationError, match="not present in"):
modeler.updated_copy(run_only=("nonexistent_wave_port",))
48 changes: 48 additions & 0 deletions tidy3d/plugins/smatrix/component_modelers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,35 @@ def _validate_element_mappings(cls, element_mappings, values):
)
return element_mappings

@pd.validator("run_only", always=True)
@skip_if_fields_missing(["ports"])
def _validate_run_only(cls, val, values):
"""Validate that run_only entries are unique and exist in matrix_indices_monitor."""
if val is None:
return val

# Check uniqueness
if len(val) != len(set(val)):
duplicates = [idx for idx in set(val) if val.count(idx) > 1]
raise SetupError(
f"'run_only' contains duplicate entries: {duplicates}. "
"Each index must appear only once."
)

# Check membership - use the helper method to get valid indices
ports = values["ports"]

valid_indices = set(cls._construct_matrix_indices_monitor(ports))
invalid_indices = [idx for idx in val if idx not in valid_indices]

if invalid_indices:
raise SetupError(
f"'run_only' contains indices {invalid_indices} that are not present in "
f"'matrix_indices_monitor'. Valid indices are: {sorted(valid_indices)}"
)

return val

_freqs_not_empty = validate_freqs_not_empty()
_freqs_lower_bound = validate_freqs_min()
_freqs_unique = validate_freqs_unique()
Expand Down Expand Up @@ -206,6 +235,25 @@ def get_port_by_name(self, port_name: str) -> Port:
raise Tidy3dKeyError(f'Port "{port_name}" not found.')
return ports[0]

@staticmethod
@abstractmethod
def _construct_matrix_indices_monitor(ports: tuple) -> tuple[IndexType, ...]:
"""Construct matrix indices for monitoring from ports.

This helper method is used by both the matrix_indices_monitor property
and the run_only validator to ensure consistency.

Parameters
----------
ports : tuple
Tuple of port objects.

Returns
-------
tuple[IndexType, ...]
Tuple of matrix indices for monitoring.
"""

@property
@abstractmethod
def matrix_indices_monitor(self) -> tuple[IndexType, ...]:
Expand Down
26 changes: 21 additions & 5 deletions tidy3d/plugins/smatrix/component_modelers/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@ def sim_dict(self) -> SimulationMap:
sim_dict[task_name] = sim_copy
return SimulationMap(keys=tuple(sim_dict.keys()), values=tuple(sim_dict.values()))

@staticmethod
def _construct_matrix_indices_monitor(ports: tuple[Port, ...]) -> tuple[MatrixIndex, ...]:
"""Construct matrix indices for monitoring from modal ports.

Parameters
----------
ports : tuple[Port, ...]
Tuple of Port objects.

Returns
-------
tuple[MatrixIndex, ...]
Tuple of (port_name, mode_index) pairs.
"""
matrix_indices = []
for port in ports:
for mode_index in range(port.mode_spec.num_modes):
matrix_indices.append((port.name, mode_index))
return tuple(matrix_indices)

@cached_property
def matrix_indices_monitor(self) -> tuple[MatrixIndex, ...]:
"""Returns a tuple of all possible matrix indices for monitoring.
Expand All @@ -98,11 +118,7 @@ def matrix_indices_monitor(self) -> tuple[MatrixIndex, ...]:
Tuple[MatrixIndex, ...]
A tuple of all possible matrix indices for the monitoring ports.
"""
matrix_indices = []
for port in self.ports:
for mode_index in range(port.mode_spec.num_modes):
matrix_indices.append((port.name, mode_index))
return tuple(matrix_indices)
return self._construct_matrix_indices_monitor(self.ports)

@cached_property
def matrix_indices_source(self) -> tuple[MatrixIndex, ...]:
Expand Down
30 changes: 24 additions & 6 deletions tidy3d/plugins/smatrix/component_modelers/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,17 +256,35 @@ def network_dict(self) -> dict[NetworkIndex, tuple[TerminalPortType, int]]:
network_dict[key] = (port, mode_index)
return network_dict

@cached_property
def matrix_indices_monitor(self) -> tuple[NetworkIndex, ...]:
"""Tuple of all the possible matrix indices."""
@staticmethod
def _construct_matrix_indices_monitor(
ports: tuple[TerminalPortType, ...],
) -> tuple[NetworkIndex, ...]:
"""Construct matrix indices for monitoring from terminal ports.

Parameters
----------
ports : tuple[TerminalPortType, ...]
Tuple of terminal port objects (LumpedPort, CoaxialLumpedPort, or WavePort).

Returns
-------
tuple[NetworkIndex, ...]
Tuple of network index strings.
"""
matrix_indices = []
for port in self.ports:
for port in ports:
if isinstance(port, WavePort):
matrix_indices.append(self.network_index(port, port.mode_index))
matrix_indices.append(TerminalComponentModeler.network_index(port, port.mode_index))
else:
matrix_indices.append(self.network_index(port))
matrix_indices.append(TerminalComponentModeler.network_index(port))
return tuple(matrix_indices)

@cached_property
def matrix_indices_monitor(self) -> tuple[NetworkIndex, ...]:
"""Tuple of all the possible matrix indices."""
return self._construct_matrix_indices_monitor(self.ports)

@cached_property
def matrix_indices_source(self) -> tuple[NetworkIndex, ...]:
"""Tuple of all the source matrix indices, which may be less than the total number of
Expand Down