Skip to content

Commit 4d28758

Browse files
committed
add selective simulation capabilities to TerminalComponentModeler
refactored common code into AbstractComponentModeler add toggle for controlling how s matrix is calculated
1 parent dbe5229 commit 4d28758

File tree

6 files changed

+302
-151
lines changed

6 files changed

+302
-151
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ with fewer layers than recommended.
6262
- Added heat sources with custom spatial dependence. It is now possible to add a `SpatialDataArray` as the `rate` in a `HeatSource`.
6363
- Added Transient Heat simulations. It is now possible to run transient Heat simulations. This can be done by specifying `analysis_spec` of `HeatChargeSimulation` object as `UnsteadyHeatAnalysis`.
6464
- A `num_grid_cells` field to `WavePort`, which ensures that there are 5 grid cells across the port. Grid refinement can be disabled by passing `None` to `num_grid_cells`.
65+
- Added Transient Heat simulations. It is now possible to run transient Heat simulations. This can be done by specifying `analysis_spec` of `HeatChargeSimulation` object as `UnsteadyHeatAnalysis`.
66+
- Selective simulation capabilities to `TerminalComponentModeler` via `run_only` and `element_mappings` fields, allowing users to run fewer simulations and extract only needed scattering matrix elements.
6567

6668
### Fixed
6769
- Fixed bug in broadband adjoint source creation when forward simulation had a pulse amplitude greater than 1 or a nonzero pulse phase.

tests/test_plugins/smatrix/terminal_component_modeler_def.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def make_port(center, direction, type, name) -> Union[CoaxialLumpedPort, WavePor
276276
inner_diameter=2 * Rinner,
277277
normal_axis=2,
278278
direction=direction,
279-
name=name,
279+
name="coax" + name,
280280
num_grid_cells=port_cells,
281281
impedance=reference_impedance,
282282
)
@@ -311,7 +311,7 @@ def make_port(center, direction, type, name) -> Union[CoaxialLumpedPort, WavePor
311311
center=center,
312312
size=[2 * Router, 2 * Router, 0],
313313
direction=direction,
314-
name=name,
314+
name="wave" + name,
315315
mode_spec=td.ModeSpec(num_modes=1),
316316
mode_index=0,
317317
voltage_integral=voltage_integral,
@@ -321,9 +321,9 @@ def make_port(center, direction, type, name) -> Union[CoaxialLumpedPort, WavePor
321321
return port
322322

323323
center_src1 = [0, 0, -length / 2]
324-
port_1 = make_port(center_src1, direction="+", type=port_types[0], name="coax_port_1")
324+
port_1 = make_port(center_src1, direction="+", type=port_types[0], name="_1")
325325
center_src2 = [0, 0, length / 2]
326-
port_2 = make_port(center_src2, direction="-", type=port_types[1], name="coax_port_2")
326+
port_2 = make_port(center_src2, direction="-", type=port_types[1], name="_2")
327327
ports = [port_1, port_2]
328328
freqs = np.linspace(freq_start, freq_stop, 100)
329329

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,3 +900,44 @@ def test_get_combined_antenna_parameters_data(monkeypatch, tmp_path):
900900
assert not np.allclose(
901901
antenna_params.radiation_efficiency, single_port_params.radiation_efficiency
902902
)
903+
904+
905+
def test_run_only_and_element_mappings(monkeypatch, tmp_path):
906+
"""Checks the terminal component modeler works when running with a subset of excitations."""
907+
z_grid = td.UniformGrid(dl=1 * 1e3)
908+
xy_grid = td.UniformGrid(dl=0.1 * 1e3)
909+
grid_spec = td.GridSpec(grid_x=xy_grid, grid_y=xy_grid, grid_z=z_grid)
910+
modeler = make_coaxial_component_modeler(
911+
path_dir=str(tmp_path), port_types=(CoaxialLumpedPort, WavePort), grid_spec=grid_spec
912+
)
913+
port0_idx = modeler.network_index(modeler.ports[0])
914+
port1_idx = modeler.network_index(modeler.ports[1])
915+
modeler_run1 = modeler.updated_copy(run_only=(port0_idx,))
916+
917+
# Make sure the smatrix and impedance calculations work for reduced simulations
918+
s_matrix = run_component_modeler(monkeypatch, modeler_run1)
919+
with pytest.raises(ValueError):
920+
TerminalComponentModeler._validate_square_matrix(s_matrix, "test_method")
921+
_ = modeler_run1.port_reference_impedances
922+
923+
assert len(modeler_run1.sim_dict) == 1
924+
S11 = (port0_idx, port0_idx)
925+
S21 = (port1_idx, port0_idx)
926+
S12 = (port0_idx, port1_idx)
927+
S22 = (port1_idx, port1_idx)
928+
element_mappings = ((S11, S22, 1),)
929+
modeler_with_mappings = modeler.updated_copy(element_mappings=element_mappings)
930+
assert len(modeler_with_mappings.sim_dict) == 2
931+
932+
# Column 1 is mapped to column 2, resulting in one simulation
933+
element_mappings = ((S11, S22, 1), (S21, S12, 1))
934+
modeler_with_mappings = modeler.updated_copy(element_mappings=element_mappings)
935+
s_matrix = run_component_modeler(monkeypatch, modeler_with_mappings)
936+
assert np.all(s_matrix.values[:, 0, 0] == s_matrix.values[:, 1, 1])
937+
assert np.all(s_matrix.values[:, 0, 1] == s_matrix.values[:, 1, 0])
938+
assert len(modeler_with_mappings.sim_dict) == 1
939+
940+
# Mapping is incomplete, so two simulations are run
941+
element_mappings = ((S11, S22, 1), (S12, S21, 1))
942+
modeler_with_mappings = modeler.updated_copy(element_mappings=element_mappings)
943+
assert len(modeler_with_mappings.sim_dict) == 2

tidy3d/plugins/smatrix/component_modelers/base.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import os
66
from abc import ABC, abstractmethod
7-
from typing import Optional, Union, get_args
7+
from typing import Generic, Optional, TypeVar, Union, get_args
88

99
import numpy as np
1010
import pydantic.v1 as pd
@@ -13,7 +13,8 @@
1313
from tidy3d.components.data.data_array import DataArray
1414
from tidy3d.components.data.sim_data import SimulationData
1515
from tidy3d.components.simulation import Simulation
16-
from tidy3d.components.types import FreqArray
16+
from tidy3d.components.types import Complex, FreqArray
17+
from tidy3d.components.validators import assert_unique_names
1718
from tidy3d.config import config
1819
from tidy3d.constants import HERTZ
1920
from tidy3d.exceptions import SetupError, Tidy3dKeyError
@@ -31,8 +32,12 @@
3132
LumpedPortType = Union[LumpedPort, CoaxialLumpedPort]
3233
TerminalPortType = Union[LumpedPortType, WavePort]
3334

35+
# Generic type variables for matrix indices and elements
36+
IndexType = TypeVar("IndexType")
37+
ElementType = TypeVar("ElementType")
3438

35-
class AbstractComponentModeler(ABC, Tidy3dBaseModel):
39+
40+
class AbstractComponentModeler(ABC, Generic[IndexType, ElementType], Tidy3dBaseModel):
3641
"""Tool for modeling devices and computing port parameters."""
3742

3843
simulation: Simulation = pd.Field(
@@ -108,6 +113,24 @@ class AbstractComponentModeler(ABC, Tidy3dBaseModel):
108113
"fields that were not used to create the task will cause errors.",
109114
)
110115

116+
element_mappings: tuple[tuple[ElementType, ElementType, Complex], ...] = pd.Field(
117+
(),
118+
title="Element Mappings",
119+
description="Tuple of S matrix element mappings, each described by a tuple of "
120+
"(input_element, output_element, coefficient), where the coefficient is the "
121+
"element_mapping coefficient describing the relationship between the input and output "
122+
"matrix element. If all elements of a given column of the scattering matrix are defined "
123+
"by ``element_mappings``, the simulation corresponding to this column is skipped automatically.",
124+
)
125+
126+
run_only: Optional[tuple[IndexType, ...]] = pd.Field(
127+
None,
128+
title="Run Only",
129+
description="Set of matrix indices that define the simulations to run. "
130+
"If ``None``, simulations will be run for all indices in the scattering matrix. "
131+
"If a tuple is given, simulations will be run only for the given matrix indices.",
132+
)
133+
111134
@pd.validator("simulation", always=True)
112135
def _sim_has_no_sources(cls, val):
113136
"""Make sure simulation has no sources as they interfere with tool."""
@@ -230,6 +253,44 @@ def get_port_by_name(self, port_name: str) -> Port:
230253
raise Tidy3dKeyError(f'Port "{port_name}" not found.')
231254
return ports[0]
232255

256+
@property
257+
@abstractmethod
258+
def matrix_indices_monitor(self) -> tuple[IndexType, ...]:
259+
"""Abstract property for all matrix indices that will be used to collect data."""
260+
261+
@cached_property
262+
def matrix_indices_source(self) -> tuple[IndexType, ...]:
263+
"""Tuple of all the source matrix indices, which may be less than the total number of ports."""
264+
if self.run_only is not None:
265+
return self.run_only
266+
return self.matrix_indices_monitor
267+
268+
@cached_property
269+
def matrix_indices_run_sim(self) -> tuple[IndexType, ...]:
270+
"""Tuple of all the matrix indices that will be used to run simulations."""
271+
272+
if not self.element_mappings:
273+
return self.matrix_indices_source
274+
275+
# all the (i, j) pairs in `S_ij` that are tagged as covered by `element_mappings`
276+
elements_determined_by_map = [element_out for (_, element_out, _) in self.element_mappings]
277+
278+
# loop through rows of the full s matrix and record rows that still need running.
279+
source_indices_needed = []
280+
for col_index in self.matrix_indices_source:
281+
# loop through columns and keep track of whether each element is covered by mapping.
282+
matrix_elements_covered = []
283+
for row_index in self.matrix_indices_monitor:
284+
element = (row_index, col_index)
285+
element_covered_by_map = element in elements_determined_by_map
286+
matrix_elements_covered.append(element_covered_by_map)
287+
288+
# if any matrix elements in row still not covered by map, a source is needed for row.
289+
if not all(matrix_elements_covered):
290+
source_indices_needed.append(col_index)
291+
292+
return source_indices_needed
293+
233294
@abstractmethod
234295
def _construct_smatrix(self, batch_data: BatchData) -> DataArray:
235296
"""Post process :class:`.BatchData` to generate scattering matrix."""
@@ -308,3 +369,5 @@ def sim_data_by_task_name(self, task_name: str) -> SimulationData:
308369
sim_data = self.batch_data[task_name]
309370
config.logging_level = log_level_cache
310371
return sim_data
372+
373+
_unique_port_names = assert_unique_names("ports")

tidy3d/plugins/smatrix/component_modelers/modal.py

Lines changed: 2 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
from tidy3d.components.simulation import Simulation
1616
from tidy3d.components.source.field import ModeSource
1717
from tidy3d.components.source.time import GaussianPulse
18-
from tidy3d.components.types import Ax, Complex
18+
from tidy3d.components.types import Ax
1919
from tidy3d.components.viz import add_ax_if_none, equal_aspect
20-
from tidy3d.exceptions import SetupError
2120
from tidy3d.plugins.smatrix.ports.modal import ModalPortDataArray, Port
2221
from tidy3d.web.api.container import BatchData
2322

@@ -27,7 +26,7 @@
2726
Element = tuple[MatrixIndex, MatrixIndex] # the 'ij' in S_ij
2827

2928

30-
class ComponentModeler(AbstractComponentModeler):
29+
class ComponentModeler(AbstractComponentModeler[MatrixIndex, Element]):
3130
"""
3231
Tool for modeling devices and computing scattering matrix elements.
3332
@@ -47,52 +46,6 @@ class ComponentModeler(AbstractComponentModeler):
4746
"For each input mode, one simulation will be run with a modal source.",
4847
)
4948

50-
element_mappings: tuple[tuple[Element, Element, Complex], ...] = pd.Field(
51-
(),
52-
title="Element Mappings",
53-
description="Mapping between elements of the scattering matrix, "
54-
"as specified by pairs of ``(port name, mode index)`` matrix indices, where the "
55-
"first element of the pair is the output and the second element of the pair is the input."
56-
"Each item of ``element_mappings`` is a tuple of ``(element1, element2, c)``, where "
57-
"the scattering matrix ``Smatrix[element2]`` is set equal to ``c * Smatrix[element1]``."
58-
"If all elements of a given column of the scattering matrix are defined by "
59-
" ``element_mappings``, the simulation corresponding to this column "
60-
"is skipped automatically.",
61-
)
62-
63-
run_only: Optional[tuple[MatrixIndex, ...]] = pd.Field(
64-
None,
65-
title="Run Only",
66-
description="If given, a tuple of matrix indices, specified by (:class:`.Port`, ``int``),"
67-
" to run only, excluding the other rows from the scattering matrix. "
68-
"If this option is used, "
69-
"the data corresponding to other inputs will be missing in the resulting matrix.",
70-
)
71-
"""Finally, to exclude some rows of the scattering matrix, one can supply a ``run_only`` parameter to the
72-
:class:`ComponentModeler`. ``run_only`` contains the scattering matrix indices that the user wants to run as a
73-
source. If any indices are excluded, they will not be run."""
74-
75-
verbose: bool = pd.Field(
76-
False,
77-
title="Verbosity",
78-
description="Whether the :class:`.ComponentModeler` should print status and progressbars.",
79-
)
80-
81-
callback_url: str = pd.Field(
82-
None,
83-
title="Callback URL",
84-
description="Http PUT url to receive simulation finish event. "
85-
"The body content is a json file with fields "
86-
"``{'id', 'status', 'name', 'workUnit', 'solverVersion'}``.",
87-
)
88-
89-
@pd.validator("simulation", always=True)
90-
def _sim_has_no_sources(cls, val):
91-
"""Make sure simulation has no sources as they interfere with tool."""
92-
if len(val.sources) > 0:
93-
raise SetupError("'ComponentModeler.simulation' must not have any sources.")
94-
return val
95-
9649
@cached_property
9750
def sim_dict(self) -> dict[str, Simulation]:
9851
"""Generate all the :class:`.Simulation` objects for the S matrix calculation."""
@@ -121,39 +74,6 @@ def matrix_indices_monitor(self) -> tuple[MatrixIndex, ...]:
12174
matrix_indices.append((port.name, mode_index))
12275
return tuple(matrix_indices)
12376

124-
@cached_property
125-
def matrix_indices_source(self) -> tuple[MatrixIndex, ...]:
126-
"""Tuple of all the source matrix indices (port, mode_index) in the Component Modeler."""
127-
if self.run_only is not None:
128-
return self.run_only
129-
return self.matrix_indices_monitor
130-
131-
@cached_property
132-
def matrix_indices_run_sim(self) -> tuple[MatrixIndex, ...]:
133-
"""Tuple of all the source matrix indices (port, mode_index) in the Component Modeler."""
134-
135-
if self.element_mappings is None or self.element_mappings == {}:
136-
return self.matrix_indices_source
137-
138-
# all the (i, j) pairs in `S_ij` that are tagged as covered by `element_mappings`
139-
elements_determined_by_map = [element_out for (_, element_out, _) in self.element_mappings]
140-
141-
# loop through rows of the full s matrix and record rows that still need running.
142-
source_indices_needed = []
143-
for col_index in self.matrix_indices_source:
144-
# loop through columns and keep track of whether each element is covered by mapping.
145-
matrix_elements_covered = []
146-
for row_index in self.matrix_indices_monitor:
147-
element = (row_index, col_index)
148-
element_covered_by_map = element in elements_determined_by_map
149-
matrix_elements_covered.append(element_covered_by_map)
150-
151-
# if any matrix elements in row still not covered by map, a source is needed for row.
152-
if not all(matrix_elements_covered):
153-
source_indices_needed.append(col_index)
154-
155-
return source_indices_needed
156-
15777
@cached_property
15878
def port_names(self) -> tuple[list[str], list[str]]:
15979
"""List of port names for inputs and outputs, respectively."""

0 commit comments

Comments
 (0)