From d8bba2b3227c05ecbdb08ed61f41a201ff62e925 Mon Sep 17 00:00:00 2001 From: Momchil Minkov Date: Tue, 7 Oct 2025 10:27:32 +0200 Subject: [PATCH 1/9] feat: mode sort and filter specifications in ModeSpec --- CHANGELOG.md | 1 + schemas/EMESimulation.json | 80 +++++++++++++++++++ schemas/ModeSimulation.json | 75 ++++++++++++++++++ schemas/Simulation.json | 75 ++++++++++++++++++ schemas/TerminalComponentModeler.json | 76 ++++++++++++++++++ tests/test_components/test_mode.py | 4 +- tests/test_plugins/test_mode_solver.py | 104 +++++++++++++++++++++++++ tidy3d/__init__.py | 3 +- tidy3d/components/data/monitor_data.py | 26 ++++++- tidy3d/components/mode/mode_solver.py | 96 ++++++++++++++++++++--- tidy3d/components/mode_spec.py | 84 +++++++++++++++++++- 11 files changed, 606 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5094fd2f58..c97db8c362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for `tidy3d-extras`, an optional plugin that enables more accurate local mode solving via subpixel averaging. - Added support for `symlog` and `log` scale plotting in `Scene.plot_eps()` and `Scene.plot_structures_property()` methods. The `symlog` scale provides linear behavior near zero and logarithmic behavior elsewhere, while 'log' is a base 10 logarithmic scale. +- `ModeFilterSpec` 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 `filter_spec=ModeFilterSpec(filter_key="TE_polarization", filter_reference=0.5)`. ### 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`. diff --git a/schemas/EMESimulation.json b/schemas/EMESimulation.json index df1713e027..ffd994db36 100644 --- a/schemas/EMESimulation.json +++ b/schemas/EMESimulation.json @@ -4786,6 +4786,13 @@ ], "type": "string" }, + "filter_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeFilterSpec" + } + ] + }, "group_index_step": { "anyOf": [ { @@ -7484,6 +7491,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -7519,6 +7527,70 @@ ], "type": "object" }, + "ModeFilterSpec": { + "additionalProperties": false, + "properties": { + "attrs": { + "default": {}, + "type": "object" + }, + "filter_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "filter_order": { + "default": "over", + "enum": [ + "over", + "under" + ], + "type": "string" + }, + "filter_reference": { + "default": 0.0, + "type": "number" + }, + "sort_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "sort_order": { + "default": "descending", + "enum": [ + "ascending", + "descending" + ], + "type": "string" + }, + "sort_reference": { + "type": "number" + }, + "type": { + "default": "ModeFilterSpec", + "enum": [ + "ModeFilterSpec" + ], + "type": "string" + } + }, + "type": "object" + }, "ModeSolverMonitor": { "additionalProperties": false, "properties": { @@ -7684,6 +7756,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -7805,6 +7878,13 @@ ], "type": "string" }, + "filter_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeFilterSpec" + } + ] + }, "group_index_step": { "anyOf": [ { diff --git a/schemas/ModeSimulation.json b/schemas/ModeSimulation.json index ff8fac73e2..c8273c10d6 100644 --- a/schemas/ModeSimulation.json +++ b/schemas/ModeSimulation.json @@ -6715,6 +6715,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -6750,6 +6751,70 @@ ], "type": "object" }, + "ModeFilterSpec": { + "additionalProperties": false, + "properties": { + "attrs": { + "default": {}, + "type": "object" + }, + "filter_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "filter_order": { + "default": "over", + "enum": [ + "over", + "under" + ], + "type": "string" + }, + "filter_reference": { + "default": 0.0, + "type": "number" + }, + "sort_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "sort_order": { + "default": "descending", + "enum": [ + "ascending", + "descending" + ], + "type": "string" + }, + "sort_reference": { + "type": "number" + }, + "type": { + "default": "ModeFilterSpec", + "enum": [ + "ModeFilterSpec" + ], + "type": "string" + } + }, + "type": "object" + }, "ModeMonitor": { "additionalProperties": false, "properties": { @@ -6885,6 +6950,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -7135,6 +7201,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -7309,6 +7376,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -7449,6 +7517,13 @@ ], "type": "string" }, + "filter_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeFilterSpec" + } + ] + }, "group_index_step": { "anyOf": [ { diff --git a/schemas/Simulation.json b/schemas/Simulation.json index 0e4e86b99c..dde5295279 100644 --- a/schemas/Simulation.json +++ b/schemas/Simulation.json @@ -10199,6 +10199,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10234,6 +10235,70 @@ ], "type": "object" }, + "ModeFilterSpec": { + "additionalProperties": false, + "properties": { + "attrs": { + "default": {}, + "type": "object" + }, + "filter_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "filter_order": { + "default": "over", + "enum": [ + "over", + "under" + ], + "type": "string" + }, + "filter_reference": { + "default": 0.0, + "type": "number" + }, + "sort_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "sort_order": { + "default": "descending", + "enum": [ + "ascending", + "descending" + ], + "type": "string" + }, + "sort_reference": { + "type": "number" + }, + "type": { + "default": "ModeFilterSpec", + "enum": [ + "ModeFilterSpec" + ], + "type": "string" + } + }, + "type": "object" + }, "ModeMonitor": { "additionalProperties": false, "properties": { @@ -10369,6 +10434,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10619,6 +10685,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10793,6 +10860,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10933,6 +11001,13 @@ ], "type": "string" }, + "filter_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeFilterSpec" + } + ] + }, "group_index_step": { "anyOf": [ { diff --git a/schemas/TerminalComponentModeler.json b/schemas/TerminalComponentModeler.json index 8fe6ce3d51..95b55a10f4 100644 --- a/schemas/TerminalComponentModeler.json +++ b/schemas/TerminalComponentModeler.json @@ -10682,6 +10682,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10717,6 +10718,70 @@ ], "type": "object" }, + "ModeFilterSpec": { + "additionalProperties": false, + "properties": { + "attrs": { + "default": {}, + "type": "object" + }, + "filter_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "filter_order": { + "default": "over", + "enum": [ + "over", + "under" + ], + "type": "string" + }, + "filter_reference": { + "default": 0.0, + "type": "number" + }, + "sort_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "sort_order": { + "default": "descending", + "enum": [ + "ascending", + "descending" + ], + "type": "string" + }, + "sort_reference": { + "type": "number" + }, + "type": { + "default": "ModeFilterSpec", + "enum": [ + "ModeFilterSpec" + ], + "type": "string" + } + }, + "type": "object" + }, "ModeMonitor": { "additionalProperties": false, "properties": { @@ -10852,6 +10917,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -11102,6 +11168,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -11276,6 +11343,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -11416,6 +11484,13 @@ ], "type": "string" }, + "filter_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeFilterSpec" + } + ] + }, "group_index_step": { "anyOf": [ { @@ -16747,6 +16822,7 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, + "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ diff --git a/tests/test_components/test_mode.py b/tests/test_components/test_mode.py index 40eb9031d2..2d82089219 100644 --- a/tests/test_components/test_mode.py +++ b/tests/test_components/test_mode.py @@ -159,7 +159,9 @@ def test_validation_from_simulation(): def get_mode_sim(): - mode_spec = MODE_SPEC.updated_copy(filter_pol="tm") + mode_spec = MODE_SPEC.updated_copy( + filter_spec=td.ModeFilterSpec(filter_key="TM_fraction", filter_reference=0.5) + ) permittivity_monitor = td.PermittivityMonitor( size=(1, 1, 0), center=(0, 0, 0), name="eps", freqs=FS ) diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 2bd21c5d4a..4ce761a25a 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import get_args + import matplotlib.pyplot as plt import numpy as np import pydantic.v1 as pydantic @@ -12,6 +14,7 @@ from tidy3d.components.data.monitor_data import ModeSolverData from tidy3d.components.mode.derivatives import create_sfactor_b, create_sfactor_f from tidy3d.components.mode.solver import compute_modes +from tidy3d.components.mode_spec import MODE_DATA_KEYS from tidy3d.exceptions import DataError, SetupError from tidy3d.plugins.mode import ModeSolver from tidy3d.plugins.mode.mode_solver import MODE_MONITOR_NAME @@ -1299,3 +1302,104 @@ def test_translated_dot(): assert np.allclose(data2.outer_dot(data_translated), data2.outer_dot(data2), atol=atol) assert np.allclose(data_translated.outer_dot(data2), data2.outer_dot(data2), atol=atol) + + +def test_mode_spec_filter_pol_filter_spec_exclusive(): + """Ensure ModeSpec errors when both filter_pol and filter_spec are set.""" + with pytest.raises(pydantic.ValidationError, match="simultaneously"): + _ = td.ModeSpec(num_modes=1, filter_pol="te", filter_spec=td.ModeFilterSpec()) + + +def test_modes_filter_sort(): + """Test the filtering and sorting of modes.""" + simulation = td.Simulation( + size=SIM_SIZE, + grid_spec=td.GridSpec(wavelength=1.0), + structures=[WAVEGUIDE], + run_time=1e-12, + symmetry=(0, 0, 1), + boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), + sources=[SRC], + ) + mode_spec = td.ModeSpec( + num_modes=5, + target_neff=2.0, + filter_spec=td.ModeFilterSpec(sort_key="n_eff", sort_order="ascending"), + num_pml=(10, 10), + ) + ms = ModeSolver( + simulation=simulation, + plane=PLANE, + mode_spec=mode_spec, + freqs=[td.C_0 / 1.0, td.C_0 / 2.0], + direction="-", + ) + modes = ms.solve() + n_eff = modes.n_eff + print(n_eff.diff(dim="mode_index")) + assert np.all(n_eff.diff(dim="mode_index") >= 0) + + for key in get_args(MODE_DATA_KEYS): + # Test ascending + ms = ms.updated_copy( + mode_spec=mode_spec.updated_copy( + filter_spec=td.ModeFilterSpec(sort_key=key, sort_order="ascending") + ) + ) + ms._sort_modes(modes) + metric = getattr(modes, key) + assert np.all(metric.diff(dim="mode_index") >= 0) + + # Test descending + ms = ms.updated_copy( + mode_spec=mode_spec.updated_copy( + filter_spec=td.ModeFilterSpec(sort_key=key, sort_order="descending") + ) + ) + ms._sort_modes(modes) + metric = getattr(modes, key) + assert np.all(metric.diff(dim="mode_index") <= 0) + + # Test descending with a large reference value should be the same as ascending + ms = ms.updated_copy( + mode_spec=mode_spec.updated_copy( + filter_spec=td.ModeFilterSpec( + sort_key=key, sort_order="descending", sort_reference=100 + ) + ) + ) + ms._sort_modes(modes) + metric = getattr(modes, key) + assert np.all(metric.diff(dim="mode_index") >= 0) + + # Test filter + sort within groups using n_eff at first frequency + ms = ms.updated_copy(mode_spec=mode_spec) + metric = modes.n_eff.isel(f=0) + thresh = float(np.median(metric.values)) + first_group_size = int(np.sum(metric.values >= thresh)) + + ms = ms.updated_copy( + mode_spec=mode_spec.updated_copy( + filter_spec=td.ModeFilterSpec( + filter_key="n_eff", + filter_reference=thresh, + filter_order="over", + sort_key="n_eff", + sort_order="ascending", + ) + ) + ) + ms._sort_modes(modes) + metric_sorted = modes.n_eff.isel(f=0) + # first group satisfies filter + assert np.all(metric_sorted.isel(mode_index=slice(0, first_group_size)).values >= thresh) + # and is sorted ascending within the group + assert np.all( + metric_sorted.isel(mode_index=slice(0, first_group_size)).diff(dim="mode_index") >= 0 + ) + # second group satisfies filter + assert np.all(metric_sorted.isel(mode_index=slice(first_group_size, None)).values < thresh) + # and is also sorted ascending within the group + assert np.all( + metric_sorted.isel(mode_index=slice(first_group_size, None)).diff(dim="mode_index") >= 0 + ) diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 8741cec4fc..c1c0460010 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -303,7 +303,7 @@ from .components.mode.simulation import ModeSimulation # modes -from .components.mode_spec import ModeSpec +from .components.mode_spec import ModeFilterSpec, ModeSpec # monitors from .components.monitor import ( @@ -631,6 +631,7 @@ def set_logging_level(level: str) -> None: "ModeABCBoundary", "ModeAmpsDataArray", "ModeData", + "ModeFilterSpec", "ModeIndexDataArray", "ModeMonitor", "ModeSimulation", diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index 9e0a967334..606e8d9f27 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -2079,6 +2079,26 @@ def pol_fraction_waveguide(self) -> xr.Dataset: return xr.Dataset(data_vars={"te": te_frac, "tm": tm_frac}) + @property + def TE_fraction(self) -> xr.DataArray: + """Alias for ``pol_fraction.te``.""" + return self.pol_fraction["te"] + + @property + def TM_fraction(self) -> xr.DataArray: + """Alias for ``pol_fraction.tm``.""" + return self.pol_fraction["tm"] + + @property + def wg_TE_fraction(self) -> xr.DataArray: + """Alias for ``pol_fraction_waveguide.te``.""" + return self.pol_fraction_waveguide["te"] + + @property + def wg_TM_fraction(self) -> xr.DataArray: + """Alias for ``pol_fraction_waveguide.tm``.""" + return self.pol_fraction_waveguide["tm"] + @property def modes_info(self) -> xr.Dataset: """Dataset collecting various properties of the stored modes.""" @@ -2104,9 +2124,9 @@ def modes_info(self) -> xr.Dataset: if len(self.field_components) == 6: info["mode area"] = self.mode_area - info[f"TE (E{self._tangential_dims[0]}) fraction"] = self.pol_fraction["te"] - info["wg TE fraction"] = self.pol_fraction_waveguide["te"] - info["wg TM fraction"] = self.pol_fraction_waveguide["tm"] + info[f"TE (E{self._tangential_dims[0]}) fraction"] = self.TE_fraction + info["wg TE fraction"] = self.wg_TE_fraction + info["wg TM fraction"] = self.wg_TM_fraction return xr.Dataset(data_vars=info) diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index 12f511383f..bd0771be81 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -523,10 +523,12 @@ def data_raw(self) -> ModeSolverData: self._normalize_modes(mode_solver_data=mode_solver_data) # filter polarization if requested - if self.mode_spec.filter_pol is not None: - self._filter_polarization(mode_solver_data=mode_solver_data) + self._filter_polarization(mode_solver_data=mode_solver_data) - # sort modes if requested + # filter and sort modes if requested by filter_spec + self._sort_modes(mode_solver_data=mode_solver_data) + + # sort modes across frequencies if requested if self.mode_spec.track_freq and len(self.freqs) > 1: mode_solver_data = mode_solver_data.overlap_sort(self.mode_spec.track_freq) @@ -1324,12 +1326,28 @@ def _filter_components(self, mode_solver_data: ModeSolverData): } return mode_solver_data.updated_copy(**skip_components, validate=False) + def _apply_mode_reorder(self, mode_solver_data: ModeSolverData, ifreq: int, sort_inds): + """Apply a mode reordering along mode_index for a single frequency index.""" + mode_solver_data._cached_properties = {} # clear cache as we're modifying in-place + for data in [ + *list(mode_solver_data.field_components.values()), + mode_solver_data.n_complex, + mode_solver_data.grid_primal_correction, + mode_solver_data.grid_dual_correction, + ]: + if hasattr(data, "values") and data.values.ndim >= 2: + data.values[..., ifreq, :] = data.values[..., ifreq, sort_inds] + def _filter_polarization(self, mode_solver_data: ModeSolverData): """Filter polarization. Note: this modifies ``mode_solver_data`` in-place.""" + filter_pol = self.mode_spec.filter_pol + if filter_pol is None: + return + pol_frac = mode_solver_data.pol_fraction for ifreq in range(len(self.freqs)): te_frac = pol_frac.te.isel(f=ifreq) - if self.mode_spec.filter_pol == "te": + if filter_pol == "te": sort_inds = np.concatenate( ( np.where(te_frac >= 0.5)[0], @@ -1337,7 +1355,7 @@ def _filter_polarization(self, mode_solver_data: ModeSolverData): np.where(np.isnan(te_frac))[0], ) ) - elif self.mode_spec.filter_pol == "tm": + elif filter_pol == "tm": sort_inds = np.concatenate( ( np.where(te_frac <= 0.5)[0], @@ -1345,13 +1363,67 @@ def _filter_polarization(self, mode_solver_data: ModeSolverData): np.where(np.isnan(te_frac))[0], ) ) - for data in [ - *list(mode_solver_data.field_components.values()), - mode_solver_data.n_complex, - mode_solver_data.grid_primal_correction, - mode_solver_data.grid_dual_correction, - ]: - data.values[..., ifreq, :] = data.values[..., ifreq, sort_inds] + self._apply_mode_reorder(mode_solver_data, ifreq, sort_inds) + + def _sort_modes(self, mode_solver_data: ModeSolverData) -> None: + """Sort modes per frequency according to ModeSpec.filter_spec. Modifies data in place.""" + filter_spec = self.mode_spec.filter_spec + if filter_spec is None: + return + + num_freqs = len(self.freqs) + + # Helper to compute ordered indices within a subset + def _order_indices(indices, vals_all): + if indices.size == 0: + return indices + vals = vals_all.isel(mode_index=indices) + arr = vals.to_numpy() + order = np.argsort(arr) + if filter_spec.sort_order == "descending": + order = order[::-1] + return indices[order] + + # Precompute metrics if provided + filter_metric = None + sort_metric = None + if filter_spec.filter_key is not None: + filter_metric = getattr(mode_solver_data, filter_spec.filter_key) + if filter_spec.sort_key is not None: + sort_metric = getattr(mode_solver_data, filter_spec.sort_key) + + for ifreq in range(num_freqs): + all_inds = np.arange(self.mode_spec.num_modes) + + # Build groups according to filter if requested + if filter_metric is not None: + vals_filt = filter_metric.isel(f=ifreq) + # Boolean mask for modes in the first group + if filter_spec.filter_order == "over": + mask_first = (vals_filt >= filter_spec.filter_reference).to_numpy() + else: + mask_first = (vals_filt <= filter_spec.filter_reference).to_numpy() + group1 = all_inds[mask_first] + group2 = all_inds[~mask_first] + else: + group1 = all_inds + group2 = np.array([], dtype=int) + + # Sorting within each group if requested + if sort_metric is not None: + vals_sort = sort_metric.isel(f=ifreq) + # apply reference difference if requested + if filter_spec.sort_reference is not None: + vals_sort = np.abs(vals_sort - filter_spec.sort_reference) + + g1 = _order_indices(group1, vals_sort) + g2 = _order_indices(group2, vals_sort) + sort_inds = np.concatenate([g1, g2]) + else: + # only filtering applied, keep original ordering within groups + sort_inds = np.concatenate([group1, group2]) + + self._apply_mode_reorder(mode_solver_data, ifreq, sort_inds) @cached_property def data(self) -> ModeSolverData: diff --git a/tidy3d/components/mode_spec.py b/tidy3d/components/mode_spec.py index 0f85bb47c0..16bfdd8c4c 100644 --- a/tidy3d/components/mode_spec.py +++ b/tidy3d/components/mode_spec.py @@ -3,7 +3,7 @@ from __future__ import annotations from math import isclose -from typing import Literal, Union +from typing import Literal, Optional, Union import numpy as np import pydantic.v1 as pd @@ -16,6 +16,62 @@ from .types import Axis2D, TrackFreq GROUP_INDEX_STEP = 0.005 +MODE_DATA_KEYS = Literal[ + "n_eff", + "k_eff", + "TE_fraction", + "TM_fraction", + "wg_TE_fraction", + "wg_TM_fraction", + "mode_area", +] + + +class ModeFilterSpec(Tidy3dBaseModel): + """Specification for filtering and sorting modes within each frequency. + + First, an optional filtering step splits the modes into two groups based on a threshold + applied to ``filter_key``: modes "over" or "under" ``filter_reference`` are placed first, + with the remaining modes placed next. Second, an optional sorting step orders modes within + each group according to ``sort_key``, optionally with respect to ``sort_reference`` and in + the specified ``sort_order``. + """ + + # Filtering stage + filter_key: Optional[MODE_DATA_KEYS] = pd.Field( + None, + title="Filtering key", + description="Quantity used to filter modes into two groups before sorting.", + ) + filter_reference: float = pd.Field( + 0.0, + title="Filtering reference", + description="Reference value used in the filtering stage.", + ) + filter_order: Literal["over", "under"] = pd.Field( + "over", + title="Filtering order", + description="Select whether the first group contains values over or under the reference.", + ) + + # Sorting stage + sort_key: Optional[MODE_DATA_KEYS] = pd.Field( + None, + title="Sorting key", + description="Quantity used to sort modes within each filtered group.", + ) + sort_reference: Optional[float] = pd.Field( + None, + title="Sorting reference", + description=( + "If provided, sorting is based on the absolute difference to this reference value." + ), + ) + sort_order: Literal["ascending", "descending"] = pd.Field( + "descending", + title="Sorting direction", + description="Sort order for the selected key or difference to reference value.", + ) class ModeSpec(Tidy3dBaseModel): @@ -163,6 +219,15 @@ class ModeSpec(Tidy3dBaseModel): f"default of {GROUP_INDEX_STEP} is used.", ) + filter_spec: Optional[ModeFilterSpec] = pd.Field( + None, + title="Mode filtering and sorting specification", + description="Defines how to filter and sort modes within each frequency. If ``None``, " + "sorting is by descending effective index (unless overridden by other options). If ``track_freq`` " + "is not ``None``, the sorting is only exact at the specified frequency, while at other frequencies " + "it can change depending on the mode tracking.", + ) + @pd.validator("bend_axis", always=True) @skip_if_fields_missing(["bend_radius"]) def bend_axis_given(cls, val, values): @@ -235,3 +300,20 @@ def angle_rotation_with_phi(cls, val, values): "enabled." ) return val + + @pd.root_validator(skip_on_failure=True) + def _filter_pol_and_sort_spec_exclusive(cls, values): + """Ensure that 'filter_pol' and 'filter_spec' are not used together.""" + if values.get("filter_pol") is not None and values.get("filter_spec") is not None: + raise SetupError("'filter_pol' and 'filter_spec' cannot be used simultaneously.") + return values + + @pd.validator("filter_pol", always=True) + def _filter_pol_deprecated(cls, val): + """Warn that 'filter_pol' is deprecated in favor of 'filter_spec'.""" + if val is not None: + log.warning( + "'filter_pol' is deprecated and will be removed in future versions. " + "Please use 'filter_spec' instead." + ) + return val From f8afea989c6f9967316b9499b4cf01a46fd9cf71 Mon Sep 17 00:00:00 2001 From: Momchil Minkov Date: Thu, 9 Oct 2025 13:54:54 +0200 Subject: [PATCH 2/9] initial addressing of review comments --- tests/test_plugins/test_mode_solver.py | 67 ++++++++++++++++++++------ tidy3d/components/mode/mode_solver.py | 22 ++++++--- tidy3d/components/mode_spec.py | 2 +- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 4ce761a25a..8fbc43f50e 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -1376,30 +1376,69 @@ def test_modes_filter_sort(): ms = ms.updated_copy(mode_spec=mode_spec) metric = modes.n_eff.isel(f=0) thresh = float(np.median(metric.values)) - first_group_size = int(np.sum(metric.values >= thresh)) + # Test filter by n_eff and sort by k_eff ms = ms.updated_copy( mode_spec=mode_spec.updated_copy( filter_spec=td.ModeFilterSpec( filter_key="n_eff", filter_reference=thresh, filter_order="over", - sort_key="n_eff", + sort_key="k_eff", sort_order="ascending", ) ) ) ms._sort_modes(modes) - metric_sorted = modes.n_eff.isel(f=0) - # first group satisfies filter - assert np.all(metric_sorted.isel(mode_index=slice(0, first_group_size)).values >= thresh) - # and is sorted ascending within the group - assert np.all( - metric_sorted.isel(mode_index=slice(0, first_group_size)).diff(dim="mode_index") >= 0 - ) - # second group satisfies filter - assert np.all(metric_sorted.isel(mode_index=slice(first_group_size, None)).values < thresh) - # and is also sorted ascending within the group - assert np.all( - metric_sorted.isel(mode_index=slice(first_group_size, None)).diff(dim="mode_index") >= 0 + for ifreq in range(len(ms.freqs)): + metric_filtered = modes.n_eff.isel(f=ifreq) + metric_sorted = modes.k_eff.isel(f=ifreq) + first_group_size = int(np.sum(metric_filtered.values >= thresh)) + # first group satisfies filter + assert np.all(metric_filtered.isel(mode_index=slice(0, first_group_size)).values >= thresh) + # and is sorted ascending within the group + assert np.all( + metric_sorted.isel(mode_index=slice(0, first_group_size)).diff(dim="mode_index") >= 0 + ) + # second group satisfies filter + assert np.all( + metric_filtered.isel(mode_index=slice(first_group_size, None)).values < thresh + ) + # and is also sorted ascending within the group + assert np.all( + metric_sorted.isel(mode_index=slice(first_group_size, None)).diff(dim="mode_index") >= 0 + ) + + # Test filter only with filter_order="under", and default sorting order + ms = ms.updated_copy( + mode_spec=mode_spec.updated_copy( + filter_spec=td.ModeFilterSpec( + filter_key="TE_fraction", + filter_reference=0.5, + filter_order="under", + ) + ) ) + # need to solve again because the default only applies to the solver + # if the modes had been reordered previously, and sort_val is None, no sorting will happen + modes = ms.solve() + for ifreq in range(len(ms.freqs)): + metric_filtered = modes.TE_fraction.isel(f=ifreq) + metric_sorted = modes.n_eff.isel(f=ifreq) # defaults to n_eff in descending order + first_group_size = int(np.sum(metric_filtered.values <= 0.5)) + + # print(metric_filtered.values) + # print(metric_sorted.values) + + # first group satisfies filter + assert np.all(metric_filtered.isel(mode_index=slice(0, first_group_size)).values <= 0.5) + # and is sorted + assert np.all( + metric_sorted.isel(mode_index=slice(0, first_group_size)).diff(dim="mode_index") <= 0 + ) + # second group satisfies filter + assert np.all(metric_filtered.isel(mode_index=slice(first_group_size, None)).values > 0.5) + # and is also sorted + assert np.all( + metric_sorted.isel(mode_index=slice(first_group_size, None)).diff(dim="mode_index") <= 0 + ) diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index bd0771be81..13f5116daf 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -1335,7 +1335,7 @@ def _apply_mode_reorder(self, mode_solver_data: ModeSolverData, ifreq: int, sort mode_solver_data.grid_primal_correction, mode_solver_data.grid_dual_correction, ]: - if hasattr(data, "values") and data.values.ndim >= 2: + if data is not None and "mode_index" in data.dims: data.values[..., ifreq, :] = data.values[..., ifreq, sort_inds] def _filter_polarization(self, mode_solver_data: ModeSolverData): @@ -1365,8 +1365,13 @@ def _filter_polarization(self, mode_solver_data: ModeSolverData): ) self._apply_mode_reorder(mode_solver_data, ifreq, sort_inds) - def _sort_modes(self, mode_solver_data: ModeSolverData) -> None: - """Sort modes per frequency according to ModeSpec.filter_spec. Modifies data in place.""" + def _sort_modes(self, mode_solver_data: ModeSolverData, copy=False) -> None: + """Sort modes per frequency according to ModeSpec.filter_spec. + If ``copy==False``, modifies data in place. + """ + if copy: + mode_solver_data = mode_solver_data.copy() + filter_spec = self.mode_spec.filter_spec if filter_spec is None: return @@ -1378,8 +1383,7 @@ def _order_indices(indices, vals_all): if indices.size == 0: return indices vals = vals_all.isel(mode_index=indices) - arr = vals.to_numpy() - order = np.argsort(arr) + order = np.argsort(vals) if filter_spec.sort_order == "descending": order = order[::-1] return indices[order] @@ -1397,12 +1401,12 @@ def _order_indices(indices, vals_all): # Build groups according to filter if requested if filter_metric is not None: - vals_filt = filter_metric.isel(f=ifreq) + vals_filt = filter_metric.isel(f=ifreq).values # Boolean mask for modes in the first group if filter_spec.filter_order == "over": - mask_first = (vals_filt >= filter_spec.filter_reference).to_numpy() + mask_first = vals_filt >= filter_spec.filter_reference else: - mask_first = (vals_filt <= filter_spec.filter_reference).to_numpy() + mask_first = vals_filt <= filter_spec.filter_reference group1 = all_inds[mask_first] group2 = all_inds[~mask_first] else: @@ -1423,6 +1427,8 @@ def _order_indices(indices, vals_all): # only filtering applied, keep original ordering within groups sort_inds = np.concatenate([group1, group2]) + if np.all(sort_inds == all_inds): + continue # already in order, skip self._apply_mode_reorder(mode_solver_data, ifreq, sort_inds) @cached_property diff --git a/tidy3d/components/mode_spec.py b/tidy3d/components/mode_spec.py index 16bfdd8c4c..14a779ffbb 100644 --- a/tidy3d/components/mode_spec.py +++ b/tidy3d/components/mode_spec.py @@ -68,7 +68,7 @@ class ModeFilterSpec(Tidy3dBaseModel): ), ) sort_order: Literal["ascending", "descending"] = pd.Field( - "descending", + "ascending", title="Sorting direction", description="Sort order for the selected key or difference to reference value.", ) From bbe295470d2f0f05ee362bf50cef9846a9c76a94 Mon Sep 17 00:00:00 2001 From: Momchil Minkov Date: Thu, 9 Oct 2025 16:46:17 +0200 Subject: [PATCH 3/9] tmp --- tidy3d/components/mode/mode_solver.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index 13f5116daf..6be60ed61a 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -526,7 +526,7 @@ def data_raw(self) -> ModeSolverData: self._filter_polarization(mode_solver_data=mode_solver_data) # filter and sort modes if requested by filter_spec - self._sort_modes(mode_solver_data=mode_solver_data) + mode_solver_data = self._sort_modes(mode_solver_data=mode_solver_data, copy=False) # sort modes across frequencies if requested if self.mode_spec.track_freq and len(self.freqs) > 1: @@ -1371,6 +1371,8 @@ def _sort_modes(self, mode_solver_data: ModeSolverData, copy=False) -> None: """ if copy: mode_solver_data = mode_solver_data.copy() + else: + mode_solver_data._cached_properties = {} # clear cache as we're modifying in-place filter_spec = self.mode_spec.filter_spec if filter_spec is None: @@ -1423,13 +1425,18 @@ def _order_indices(indices, vals_all): g1 = _order_indices(group1, vals_sort) g2 = _order_indices(group2, vals_sort) sort_inds = np.concatenate([g1, g2]) + print(ifreq) + print(filter_metric.isel(f=ifreq).values) + print(sort_metric.isel(f=ifreq).values) + print(sort_inds) else: # only filtering applied, keep original ordering within groups sort_inds = np.concatenate([group1, group2]) if np.all(sort_inds == all_inds): - continue # already in order, skip + return mode_solver_data # already in order, skip self._apply_mode_reorder(mode_solver_data, ifreq, sort_inds) + return mode_solver_data @cached_property def data(self) -> ModeSolverData: From e69493f6e8d1fb7681c6bb6cf8ad1973038ef5ec Mon Sep 17 00:00:00 2001 From: Momchil Minkov Date: Fri, 10 Oct 2025 16:43:30 +0200 Subject: [PATCH 4/9] Remove in-place data modifications --- tests/test_plugins/test_mode_solver.py | 8 +-- tidy3d/components/data/dataset.py | 10 +++ tidy3d/components/mode/mode_solver.py | 98 ++++++++++++++++---------- 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 8fbc43f50e..7a1c425036 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -1346,7 +1346,7 @@ def test_modes_filter_sort(): filter_spec=td.ModeFilterSpec(sort_key=key, sort_order="ascending") ) ) - ms._sort_modes(modes) + modes = ms._sort_modes(modes) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") >= 0) @@ -1356,7 +1356,7 @@ def test_modes_filter_sort(): filter_spec=td.ModeFilterSpec(sort_key=key, sort_order="descending") ) ) - ms._sort_modes(modes) + modes = ms._sort_modes(modes) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") <= 0) @@ -1368,7 +1368,7 @@ def test_modes_filter_sort(): ) ) ) - ms._sort_modes(modes) + modes = ms._sort_modes(modes) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") >= 0) @@ -1389,7 +1389,7 @@ def test_modes_filter_sort(): ) ) ) - ms._sort_modes(modes) + modes = ms._sort_modes(modes) for ifreq in range(len(ms.freqs)): metric_filtered = modes.n_eff.isel(f=ifreq) metric_sorted = modes.k_eff.isel(f=ifreq) diff --git a/tidy3d/components/data/dataset.py b/tidy3d/components/data/dataset.py index a3b991b33e..f36c0c9fe3 100644 --- a/tidy3d/components/data/dataset.py +++ b/tidy3d/components/data/dataset.py @@ -39,6 +39,16 @@ class Dataset(Tidy3dBaseModel, ABC): """Abstract base class for objects that store collections of `:class:`.DataArray`s.""" + @property + def data_arrs(self) -> dict: + """Returns a dictionary of all `:class:`.DataArray`s in the dataset.""" + data_arrs = {} + for key in self.__fields__.keys(): + data = getattr(self, key) + if isinstance(data, DataArray): + data_arrs[key] = data + return data_arrs + class AbstractFieldDataset(Dataset, ABC): """Collection of scalar fields with some symmetry properties.""" diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index 6be60ed61a..30864a6e00 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -523,10 +523,10 @@ def data_raw(self) -> ModeSolverData: self._normalize_modes(mode_solver_data=mode_solver_data) # filter polarization if requested - self._filter_polarization(mode_solver_data=mode_solver_data) + mode_solver_data = self._filter_polarization(mode_solver_data=mode_solver_data) # filter and sort modes if requested by filter_spec - mode_solver_data = self._sort_modes(mode_solver_data=mode_solver_data, copy=False) + mode_solver_data = self._sort_modes(mode_solver_data=mode_solver_data) # sort modes across frequencies if requested if self.mode_spec.track_freq and len(self.freqs) > 1: @@ -1326,27 +1326,47 @@ def _filter_components(self, mode_solver_data: ModeSolverData): } return mode_solver_data.updated_copy(**skip_components, validate=False) - def _apply_mode_reorder(self, mode_solver_data: ModeSolverData, ifreq: int, sort_inds): - """Apply a mode reordering along mode_index for a single frequency index.""" - mode_solver_data._cached_properties = {} # clear cache as we're modifying in-place - for data in [ - *list(mode_solver_data.field_components.values()), - mode_solver_data.n_complex, - mode_solver_data.grid_primal_correction, - mode_solver_data.grid_dual_correction, - ]: - if data is not None and "mode_index" in data.dims: - data.values[..., ifreq, :] = data.values[..., ifreq, sort_inds] + def _apply_mode_reorder(self, mode_solver_data: ModeSolverData, sort_inds_2d): + """Apply a mode reordering along mode_index for all frequency indices. + + Parameters + ---------- + mode_solver_data : ModeSolverData + Data to be modified. + sort_inds_2d : np.ndarray + Array of shape (num_freqs, num_modes) where each row is the + permutation to apply to the mode_index for that frequency. + """ + num_freqs, num_modes = sort_inds_2d.shape + modify_data = {} + for key, data in mode_solver_data.data_arrs.items(): + if "mode_index" not in data.dims or "f" not in data.dims: + continue + dims_orig = data.dims + f_coord = data.coords["f"] + slices = [] + for ifreq in range(num_freqs): + sl = data.isel(f=ifreq, mode_index=sort_inds_2d[ifreq]) + slices.append(sl.assign_coords(mode_index=np.arange(num_modes))) + # Concatenate along the 'f' dimension name and then restore original frequency coordinates + data = xr.concat(slices, dim="f").assign_coords(f=f_coord).transpose(*dims_orig) + modify_data[key] = data + return mode_solver_data.updated_copy(**modify_data) def _filter_polarization(self, mode_solver_data: ModeSolverData): - """Filter polarization. Note: this modifies ``mode_solver_data`` in-place.""" + """Filter polarization.""" filter_pol = self.mode_spec.filter_pol if filter_pol is None: - return + return mode_solver_data + + num_freqs = len(self.freqs) + num_modes = self.mode_spec.num_modes + identity = np.arange(num_modes) + sort_inds_2d = np.tile(identity, (num_freqs, 1)) pol_frac = mode_solver_data.pol_fraction - for ifreq in range(len(self.freqs)): - te_frac = pol_frac.te.isel(f=ifreq) + for ifreq in range(num_freqs): + te_frac = pol_frac.te.isel(f=ifreq).values if filter_pol == "te": sort_inds = np.concatenate( ( @@ -1363,22 +1383,31 @@ def _filter_polarization(self, mode_solver_data: ModeSolverData): np.where(np.isnan(te_frac))[0], ) ) - self._apply_mode_reorder(mode_solver_data, ifreq, sort_inds) + else: + sort_inds = identity + sort_inds_2d[ifreq, : len(sort_inds)] = sort_inds + + # If no reordering needed across all frequencies, skip + if np.all(sort_inds_2d == np.tile(identity, (num_freqs, 1))): + return mode_solver_data + + return self._apply_mode_reorder(mode_solver_data, sort_inds_2d) - def _sort_modes(self, mode_solver_data: ModeSolverData, copy=False) -> None: + def _sort_modes(self, mode_solver_data: ModeSolverData) -> ModeSolverData: """Sort modes per frequency according to ModeSpec.filter_spec. - If ``copy==False``, modifies data in place. - """ - if copy: - mode_solver_data = mode_solver_data.copy() - else: - mode_solver_data._cached_properties = {} # clear cache as we're modifying in-place + Returns the (possibly reordered) mode_solver_data. + """ filter_spec = self.mode_spec.filter_spec + # Always return the original data if no filter spec if filter_spec is None: - return + return mode_solver_data num_freqs = len(self.freqs) + num_modes = self.mode_spec.num_modes + all_inds = np.arange(num_modes) + identity = np.arange(num_modes) + sort_inds_2d = np.tile(identity, (num_freqs, 1)) # Helper to compute ordered indices within a subset def _order_indices(indices, vals_all): @@ -1399,8 +1428,6 @@ def _order_indices(indices, vals_all): sort_metric = getattr(mode_solver_data, filter_spec.sort_key) for ifreq in range(num_freqs): - all_inds = np.arange(self.mode_spec.num_modes) - # Build groups according to filter if requested if filter_metric is not None: vals_filt = filter_metric.isel(f=ifreq).values @@ -1418,26 +1445,23 @@ def _order_indices(indices, vals_all): # Sorting within each group if requested if sort_metric is not None: vals_sort = sort_metric.isel(f=ifreq) - # apply reference difference if requested if filter_spec.sort_reference is not None: vals_sort = np.abs(vals_sort - filter_spec.sort_reference) - g1 = _order_indices(group1, vals_sort) g2 = _order_indices(group2, vals_sort) sort_inds = np.concatenate([g1, g2]) - print(ifreq) - print(filter_metric.isel(f=ifreq).values) - print(sort_metric.isel(f=ifreq).values) - print(sort_inds) else: # only filtering applied, keep original ordering within groups sort_inds = np.concatenate([group1, group2]) - if np.all(sort_inds == all_inds): - return mode_solver_data # already in order, skip - self._apply_mode_reorder(mode_solver_data, ifreq, sort_inds) + sort_inds_2d[ifreq, : len(sort_inds)] = sort_inds + + # If all rows are identity, skip + if np.all(sort_inds_2d == np.tile(identity, (num_freqs, 1))): return mode_solver_data + return self._apply_mode_reorder(mode_solver_data, sort_inds_2d) + @cached_property def data(self) -> ModeSolverData: """:class:`.ModeSolverData` containing the field and effective index data. From 89699f451872da75f1a6b7281b395e3fabe6a380 Mon Sep 17 00:00:00 2001 From: Momchil Minkov Date: Fri, 10 Oct 2025 17:06:21 +0200 Subject: [PATCH 5/9] move sorting to ModeData --- tests/test_plugins/test_mode_solver.py | 8 +- tidy3d/components/data/monitor_data.py | 94 +++++++++++++++++++++++ tidy3d/components/mode/mode_solver.py | 100 +------------------------ 3 files changed, 100 insertions(+), 102 deletions(-) diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 7a1c425036..46375e56f7 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -1346,7 +1346,7 @@ def test_modes_filter_sort(): filter_spec=td.ModeFilterSpec(sort_key=key, sort_order="ascending") ) ) - modes = ms._sort_modes(modes) + modes = modes.sort_modes(ms.mode_spec.filter_spec) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") >= 0) @@ -1356,7 +1356,7 @@ def test_modes_filter_sort(): filter_spec=td.ModeFilterSpec(sort_key=key, sort_order="descending") ) ) - modes = ms._sort_modes(modes) + modes = modes.sort_modes(ms.mode_spec.filter_spec) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") <= 0) @@ -1368,7 +1368,7 @@ def test_modes_filter_sort(): ) ) ) - modes = ms._sort_modes(modes) + modes = modes.sort_modes(ms.mode_spec.filter_spec) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") >= 0) @@ -1389,7 +1389,7 @@ def test_modes_filter_sort(): ) ) ) - modes = ms._sort_modes(modes) + modes = modes.sort_modes(ms.mode_spec.filter_spec) for ifreq in range(len(ms.freqs)): metric_filtered = modes.n_eff.isel(f=ifreq) metric_sorted = modes.k_eff.isel(f=ifreq) diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index 606e8d9f27..8d4c3cd8bc 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -18,6 +18,7 @@ from tidy3d.components.base_sim.data.monitor_data import AbstractMonitorData from tidy3d.components.grid.grid import Coords, Grid from tidy3d.components.medium import Medium, MediumType +from tidy3d.components.mode_spec import ModeFilterSpec from tidy3d.components.monitor import ( AuxFieldTimeMonitor, DiffractionMonitor, @@ -2230,6 +2231,99 @@ def _adjoint_source_amp(self, amp: DataArray, fwidth: float) -> ModeSource: return src_adj + def _apply_mode_reorder(self, sort_inds_2d): + """Apply a mode reordering along mode_index for all frequency indices. + + Parameters + ---------- + sort_inds_2d : np.ndarray + Array of shape (num_freqs, num_modes) where each row is the + permutation to apply to the mode_index for that frequency. + """ + num_freqs, num_modes = sort_inds_2d.shape + modify_data = {} + for key, data in self.data_arrs.items(): + if "mode_index" not in data.dims or "f" not in data.dims: + continue + dims_orig = data.dims + f_coord = data.coords["f"] + slices = [] + for ifreq in range(num_freqs): + sl = data.isel(f=ifreq, mode_index=sort_inds_2d[ifreq]) + slices.append(sl.assign_coords(mode_index=np.arange(num_modes))) + # Concatenate along the 'f' dimension name and then restore original frequency coordinates + data = xr.concat(slices, dim="f").assign_coords(f=f_coord).transpose(*dims_orig) + modify_data[key] = data + return self.updated_copy(**modify_data) + + def sort_modes(self, filter_spec: Optional[ModeFilterSpec] = None) -> ModeSolverData: + """Sort modes per frequency according to ModeSpec.filter_spec. + + Returns the (possibly reordered) mode_solver_data. + """ + # Always return the original data if no filter spec + if filter_spec is None: + return self + + num_freqs = self.n_eff["f"].size + num_modes = self.n_eff["mode_index"].size + all_inds = np.arange(num_modes) + identity = np.arange(num_modes) + sort_inds_2d = np.tile(identity, (num_freqs, 1)) + + # Helper to compute ordered indices within a subset + def _order_indices(indices, vals_all): + if indices.size == 0: + return indices + vals = vals_all.isel(mode_index=indices) + order = np.argsort(vals) + if filter_spec.sort_order == "descending": + order = order[::-1] + return indices[order] + + # Precompute metrics if provided + filter_metric = None + sort_metric = None + if filter_spec.filter_key is not None: + filter_metric = getattr(self, filter_spec.filter_key) + if filter_spec.sort_key is not None: + sort_metric = getattr(self, filter_spec.sort_key) + + for ifreq in range(num_freqs): + # Build groups according to filter if requested + if filter_metric is not None: + vals_filt = filter_metric.isel(f=ifreq).values + # Boolean mask for modes in the first group + if filter_spec.filter_order == "over": + mask_first = vals_filt >= filter_spec.filter_reference + else: + mask_first = vals_filt <= filter_spec.filter_reference + group1 = all_inds[mask_first] + group2 = all_inds[~mask_first] + else: + group1 = all_inds + group2 = np.array([], dtype=int) + + # Sorting within each group if requested + if sort_metric is not None: + vals_sort = sort_metric.isel(f=ifreq) + if filter_spec.sort_reference is not None: + vals_sort = np.abs(vals_sort - filter_spec.sort_reference) + g1 = _order_indices(group1, vals_sort) + g2 = _order_indices(group2, vals_sort) + sort_inds = np.concatenate([g1, g2]) + else: + # only filtering applied, keep original ordering within groups + sort_inds = np.concatenate([group1, group2]) + + sort_inds_2d[ifreq, : len(sort_inds)] = sort_inds + + # If all rows are identity, skip + if np.all(sort_inds_2d == np.tile(identity, (num_freqs, 1))): + return self + + return self._apply_mode_reorder(sort_inds_2d) + class ModeSolverData(ModeData): """ diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index 30864a6e00..64be1e2259 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -526,7 +526,7 @@ def data_raw(self) -> ModeSolverData: mode_solver_data = self._filter_polarization(mode_solver_data=mode_solver_data) # filter and sort modes if requested by filter_spec - mode_solver_data = self._sort_modes(mode_solver_data=mode_solver_data) + mode_solver_data = mode_solver_data.sort_modes(self.mode_spec.filter_spec) # sort modes across frequencies if requested if self.mode_spec.track_freq and len(self.freqs) > 1: @@ -1326,33 +1326,6 @@ def _filter_components(self, mode_solver_data: ModeSolverData): } return mode_solver_data.updated_copy(**skip_components, validate=False) - def _apply_mode_reorder(self, mode_solver_data: ModeSolverData, sort_inds_2d): - """Apply a mode reordering along mode_index for all frequency indices. - - Parameters - ---------- - mode_solver_data : ModeSolverData - Data to be modified. - sort_inds_2d : np.ndarray - Array of shape (num_freqs, num_modes) where each row is the - permutation to apply to the mode_index for that frequency. - """ - num_freqs, num_modes = sort_inds_2d.shape - modify_data = {} - for key, data in mode_solver_data.data_arrs.items(): - if "mode_index" not in data.dims or "f" not in data.dims: - continue - dims_orig = data.dims - f_coord = data.coords["f"] - slices = [] - for ifreq in range(num_freqs): - sl = data.isel(f=ifreq, mode_index=sort_inds_2d[ifreq]) - slices.append(sl.assign_coords(mode_index=np.arange(num_modes))) - # Concatenate along the 'f' dimension name and then restore original frequency coordinates - data = xr.concat(slices, dim="f").assign_coords(f=f_coord).transpose(*dims_orig) - modify_data[key] = data - return mode_solver_data.updated_copy(**modify_data) - def _filter_polarization(self, mode_solver_data: ModeSolverData): """Filter polarization.""" filter_pol = self.mode_spec.filter_pol @@ -1391,76 +1364,7 @@ def _filter_polarization(self, mode_solver_data: ModeSolverData): if np.all(sort_inds_2d == np.tile(identity, (num_freqs, 1))): return mode_solver_data - return self._apply_mode_reorder(mode_solver_data, sort_inds_2d) - - def _sort_modes(self, mode_solver_data: ModeSolverData) -> ModeSolverData: - """Sort modes per frequency according to ModeSpec.filter_spec. - - Returns the (possibly reordered) mode_solver_data. - """ - filter_spec = self.mode_spec.filter_spec - # Always return the original data if no filter spec - if filter_spec is None: - return mode_solver_data - - num_freqs = len(self.freqs) - num_modes = self.mode_spec.num_modes - all_inds = np.arange(num_modes) - identity = np.arange(num_modes) - sort_inds_2d = np.tile(identity, (num_freqs, 1)) - - # Helper to compute ordered indices within a subset - def _order_indices(indices, vals_all): - if indices.size == 0: - return indices - vals = vals_all.isel(mode_index=indices) - order = np.argsort(vals) - if filter_spec.sort_order == "descending": - order = order[::-1] - return indices[order] - - # Precompute metrics if provided - filter_metric = None - sort_metric = None - if filter_spec.filter_key is not None: - filter_metric = getattr(mode_solver_data, filter_spec.filter_key) - if filter_spec.sort_key is not None: - sort_metric = getattr(mode_solver_data, filter_spec.sort_key) - - for ifreq in range(num_freqs): - # Build groups according to filter if requested - if filter_metric is not None: - vals_filt = filter_metric.isel(f=ifreq).values - # Boolean mask for modes in the first group - if filter_spec.filter_order == "over": - mask_first = vals_filt >= filter_spec.filter_reference - else: - mask_first = vals_filt <= filter_spec.filter_reference - group1 = all_inds[mask_first] - group2 = all_inds[~mask_first] - else: - group1 = all_inds - group2 = np.array([], dtype=int) - - # Sorting within each group if requested - if sort_metric is not None: - vals_sort = sort_metric.isel(f=ifreq) - if filter_spec.sort_reference is not None: - vals_sort = np.abs(vals_sort - filter_spec.sort_reference) - g1 = _order_indices(group1, vals_sort) - g2 = _order_indices(group2, vals_sort) - sort_inds = np.concatenate([g1, g2]) - else: - # only filtering applied, keep original ordering within groups - sort_inds = np.concatenate([group1, group2]) - - sort_inds_2d[ifreq, : len(sort_inds)] = sort_inds - - # If all rows are identity, skip - if np.all(sort_inds_2d == np.tile(identity, (num_freqs, 1))): - return mode_solver_data - - return self._apply_mode_reorder(mode_solver_data, sort_inds_2d) + return mode_solver_data._apply_mode_reorder(sort_inds_2d) @cached_property def data(self) -> ModeSolverData: From 8540a455d732bf4f743896803ef76b7139600281 Mon Sep 17 00:00:00 2001 From: Momchil Minkov Date: Mon, 13 Oct 2025 13:19:05 +0200 Subject: [PATCH 6/9] Rename again to sort_spec and move track_freq inside of it --- tests/test_components/test_mode.py | 16 ++++-- tests/test_plugins/test_mode_solver.py | 60 +++++++------------ tidy3d/__init__.py | 4 +- tidy3d/components/data/monitor_data.py | 39 +++++++------ tidy3d/components/mode/mode_solver.py | 9 +-- tidy3d/components/mode_spec.py | 80 +++++++++++++++++++------- 6 files changed, 119 insertions(+), 89 deletions(-) diff --git a/tests/test_components/test_mode.py b/tests/test_components/test_mode.py index 2d82089219..b3dfbda21f 100644 --- a/tests/test_components/test_mode.py +++ b/tests/test_components/test_mode.py @@ -31,9 +31,9 @@ def test_modes(): _ = td.ModeSpec(num_modes=2) _ = td.ModeSpec(num_modes=1, target_neff=1.0) - options = [None, "lowest", "highest", "central"] - for opt in options: - _ = td.ModeSpec(num_modes=3, track_freq=opt) + # Valid options now specified via ModeSortSpec.track_freq + for opt in ["lowest", "highest", "central"]: + _ = td.ModeSpec(num_modes=3, sort_spec=td.ModeSortSpec(track_freq=opt)) with pytest.raises(pydantic.ValidationError): _ = td.ModeSpec(num_modes=3, track_freq="middle") @@ -160,7 +160,7 @@ def test_validation_from_simulation(): def get_mode_sim(): mode_spec = MODE_SPEC.updated_copy( - filter_spec=td.ModeFilterSpec(filter_key="TM_fraction", filter_reference=0.5) + sort_spec=td.ModeSortSpec(filter_key="TM_fraction", filter_reference=0.5) ) permittivity_monitor = td.PermittivityMonitor( size=(1, 1, 0), center=(0, 0, 0), name="eps", freqs=FS @@ -388,3 +388,11 @@ def test_plane_crosses_symmetry_plane_warning(monkeypatch): mode_spec=td.ModeSpec(), freqs=[td.C_0], ) + + +def test_track_freq_deprecation(): + """Ensure using ModeSpec.track_freq emits a deprecation warning.""" + from ..utils import AssertLogLevel + + with AssertLogLevel("WARNING", contains_str="deprecated"): + _ = td.ModeSpec(num_modes=3, track_freq="central") diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 46375e56f7..78d8151aca 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -692,7 +692,7 @@ def test_mode_solver_angle_bend(): bend_axis=0, angle_theta=np.pi / 3, angle_phi=np.pi, - track_freq="highest", + sort_spec=td.ModeSortSpec(track_freq="highest"), ) # put plane entirely in the symmetry quadrant rather than sitting on its center plane = td.Box(center=(0, 0.5, 0), size=(1, 0, 1)) @@ -853,7 +853,7 @@ def test_group_index(mock_remote_api, local, tmp_path): num_modes=2, target_neff=3.0, precision="double" if local else "single", - track_freq="central", + sort_spec=td.ModeSortSpec(track_freq="central"), ) if local: @@ -1304,10 +1304,10 @@ def test_translated_dot(): assert np.allclose(data_translated.outer_dot(data2), data2.outer_dot(data2), atol=atol) -def test_mode_spec_filter_pol_filter_spec_exclusive(): - """Ensure ModeSpec errors when both filter_pol and filter_spec are set.""" +def test_mode_spec_filter_pol_sort_spec_exclusive(): + """Ensure ModeSpec errors when both filter_pol and sort_spec are set.""" with pytest.raises(pydantic.ValidationError, match="simultaneously"): - _ = td.ModeSpec(num_modes=1, filter_pol="te", filter_spec=td.ModeFilterSpec()) + _ = td.ModeSpec(num_modes=1, filter_pol="te", sort_spec=td.ModeSortSpec(sort_key="n_eff")) def test_modes_filter_sort(): @@ -1324,7 +1324,7 @@ def test_modes_filter_sort(): mode_spec = td.ModeSpec( num_modes=5, target_neff=2.0, - filter_spec=td.ModeFilterSpec(sort_key="n_eff", sort_order="ascending"), + sort_spec=td.ModeSortSpec(sort_key="n_eff", sort_order="ascending"), num_pml=(10, 10), ) ms = ModeSolver( @@ -1341,34 +1341,20 @@ def test_modes_filter_sort(): for key in get_args(MODE_DATA_KEYS): # Test ascending - ms = ms.updated_copy( - mode_spec=mode_spec.updated_copy( - filter_spec=td.ModeFilterSpec(sort_key=key, sort_order="ascending") - ) - ) - modes = modes.sort_modes(ms.mode_spec.filter_spec) + sort_spec = td.ModeSortSpec(sort_key=key, sort_order="ascending") + modes = modes.sort_modes(sort_spec) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") >= 0) # Test descending - ms = ms.updated_copy( - mode_spec=mode_spec.updated_copy( - filter_spec=td.ModeFilterSpec(sort_key=key, sort_order="descending") - ) - ) - modes = modes.sort_modes(ms.mode_spec.filter_spec) + sort_spec = td.ModeSortSpec(sort_key=key, sort_order="descending") + modes = modes.sort_modes(sort_spec) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") <= 0) # Test descending with a large reference value should be the same as ascending - ms = ms.updated_copy( - mode_spec=mode_spec.updated_copy( - filter_spec=td.ModeFilterSpec( - sort_key=key, sort_order="descending", sort_reference=100 - ) - ) - ) - modes = modes.sort_modes(ms.mode_spec.filter_spec) + sort_spec = td.ModeSortSpec(sort_key=key, sort_order="descending", sort_reference=100) + modes = modes.sort_modes(sort_spec) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") >= 0) @@ -1378,18 +1364,14 @@ def test_modes_filter_sort(): thresh = float(np.median(metric.values)) # Test filter by n_eff and sort by k_eff - ms = ms.updated_copy( - mode_spec=mode_spec.updated_copy( - filter_spec=td.ModeFilterSpec( - filter_key="n_eff", - filter_reference=thresh, - filter_order="over", - sort_key="k_eff", - sort_order="ascending", - ) - ) - ) - modes = modes.sort_modes(ms.mode_spec.filter_spec) + sort_spec = td.ModeSortSpec( + filter_key="n_eff", + filter_reference=thresh, + filter_order="over", + sort_key="k_eff", + sort_order="ascending", + ) + modes = modes.sort_modes(sort_spec) for ifreq in range(len(ms.freqs)): metric_filtered = modes.n_eff.isel(f=ifreq) metric_sorted = modes.k_eff.isel(f=ifreq) @@ -1412,7 +1394,7 @@ def test_modes_filter_sort(): # Test filter only with filter_order="under", and default sorting order ms = ms.updated_copy( mode_spec=mode_spec.updated_copy( - filter_spec=td.ModeFilterSpec( + sort_spec=td.ModeSortSpec( filter_key="TE_fraction", filter_reference=0.5, filter_order="under", diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index c1c0460010..37706f00a2 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -303,7 +303,7 @@ from .components.mode.simulation import ModeSimulation # modes -from .components.mode_spec import ModeFilterSpec, ModeSpec +from .components.mode_spec import ModeSortSpec, ModeSpec # monitors from .components.monitor import ( @@ -631,7 +631,6 @@ def set_logging_level(level: str) -> None: "ModeABCBoundary", "ModeAmpsDataArray", "ModeData", - "ModeFilterSpec", "ModeIndexDataArray", "ModeMonitor", "ModeSimulation", @@ -639,6 +638,7 @@ def set_logging_level(level: str) -> None: "ModeSolverData", "ModeSolverDataset", "ModeSolverMonitor", + "ModeSortSpec", "ModeSource", "ModeSpec", "ModulationSpec", diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index 8d4c3cd8bc..f8577b55f5 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -18,7 +18,7 @@ from tidy3d.components.base_sim.data.monitor_data import AbstractMonitorData from tidy3d.components.grid.grid import Coords, Grid from tidy3d.components.medium import Medium, MediumType -from tidy3d.components.mode_spec import ModeFilterSpec +from tidy3d.components.mode_spec import ModeSortSpec from tidy3d.components.monitor import ( AuxFieldTimeMonitor, DiffractionMonitor, @@ -1889,8 +1889,9 @@ def _reorder_modes( freq_id, sorting[freq_id, :] ] - # Update mode_spec in the monitor - mode_spec = self.monitor.mode_spec.copy(update={"track_freq": track_freq}) + # Update mode_spec in the monitor; set deprecated track_freq to None + mode_spec = self.monitor.mode_spec.updated_copy(path="sort_spec", track_freq=track_freq) + mode_spec = self.monitor.mode_spec.updated_copy(track_freq=None) update_dict["monitor"] = self.monitor.copy(update={"mode_spec": mode_spec}) return self.copy(update=update_dict) @@ -2256,13 +2257,13 @@ def _apply_mode_reorder(self, sort_inds_2d): modify_data[key] = data return self.updated_copy(**modify_data) - def sort_modes(self, filter_spec: Optional[ModeFilterSpec] = None) -> ModeSolverData: - """Sort modes per frequency according to ModeSpec.filter_spec. + def sort_modes(self, sort_spec: Optional[ModeSortSpec] = None) -> ModeSolverData: + """Sort modes per frequency according to ModeSpec.sort_spec. Returns the (possibly reordered) mode_solver_data. """ # Always return the original data if no filter spec - if filter_spec is None: + if sort_spec is None: return self num_freqs = self.n_eff["f"].size @@ -2277,27 +2278,27 @@ def _order_indices(indices, vals_all): return indices vals = vals_all.isel(mode_index=indices) order = np.argsort(vals) - if filter_spec.sort_order == "descending": + if sort_spec.sort_order == "descending": order = order[::-1] return indices[order] # Precompute metrics if provided filter_metric = None sort_metric = None - if filter_spec.filter_key is not None: - filter_metric = getattr(self, filter_spec.filter_key) - if filter_spec.sort_key is not None: - sort_metric = getattr(self, filter_spec.sort_key) + if sort_spec.filter_key is not None: + filter_metric = getattr(self, sort_spec.filter_key) + if sort_spec.sort_key is not None: + sort_metric = getattr(self, sort_spec.sort_key) for ifreq in range(num_freqs): # Build groups according to filter if requested if filter_metric is not None: vals_filt = filter_metric.isel(f=ifreq).values # Boolean mask for modes in the first group - if filter_spec.filter_order == "over": - mask_first = vals_filt >= filter_spec.filter_reference + if sort_spec.filter_order == "over": + mask_first = vals_filt >= sort_spec.filter_reference else: - mask_first = vals_filt <= filter_spec.filter_reference + mask_first = vals_filt <= sort_spec.filter_reference group1 = all_inds[mask_first] group2 = all_inds[~mask_first] else: @@ -2307,8 +2308,8 @@ def _order_indices(indices, vals_all): # Sorting within each group if requested if sort_metric is not None: vals_sort = sort_metric.isel(f=ifreq) - if filter_spec.sort_reference is not None: - vals_sort = np.abs(vals_sort - filter_spec.sort_reference) + if sort_spec.sort_reference is not None: + vals_sort = np.abs(vals_sort - sort_spec.sort_reference) g1 = _order_indices(group1, vals_sort) g2 = _order_indices(group2, vals_sort) sort_inds = np.concatenate([g1, g2]) @@ -2322,7 +2323,9 @@ def _order_indices(indices, vals_all): if np.all(sort_inds_2d == np.tile(identity, (num_freqs, 1))): return self - return self._apply_mode_reorder(sort_inds_2d) + data_sorted = self._apply_mode_reorder(sort_inds_2d) + monitor_updated = data_sorted.monitor.updated_copy(path="mode_spec", sort_spec=sort_spec) + return data_sorted._updated({"monitor": monitor_updated}) class ModeSolverData(ModeData): @@ -3102,7 +3105,7 @@ def tangential_dims(self): return tangential_dims @property - def poynting(self) -> DataArray: + def poynting(self) -> ScalarFieldDataArray: """Time-averaged Poynting vector for field data associated to a Cartesian field projection monitor.""" fc = self.fields_cartesian dim1, dim2 = self.tangential_dims diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index 64be1e2259..ccc6cb2daf 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -525,12 +525,13 @@ def data_raw(self) -> ModeSolverData: # filter polarization if requested mode_solver_data = self._filter_polarization(mode_solver_data=mode_solver_data) - # filter and sort modes if requested by filter_spec - mode_solver_data = mode_solver_data.sort_modes(self.mode_spec.filter_spec) + # filter and sort modes if requested by sort_spec + mode_solver_data = mode_solver_data.sort_modes(self.mode_spec.sort_spec) # sort modes across frequencies if requested - if self.mode_spec.track_freq and len(self.freqs) > 1: - mode_solver_data = mode_solver_data.overlap_sort(self.mode_spec.track_freq) + track_freq = self.mode_spec._track_freq + if track_freq and len(self.freqs) > 1: + mode_solver_data = mode_solver_data.overlap_sort(track_freq) self._field_decay_warning(mode_solver_data.symmetry_expanded) diff --git a/tidy3d/components/mode_spec.py b/tidy3d/components/mode_spec.py index 14a779ffbb..449079a551 100644 --- a/tidy3d/components/mode_spec.py +++ b/tidy3d/components/mode_spec.py @@ -27,7 +27,7 @@ ] -class ModeFilterSpec(Tidy3dBaseModel): +class ModeSortSpec(Tidy3dBaseModel): """Specification for filtering and sorting modes within each frequency. First, an optional filtering step splits the modes into two groups based on a threshold @@ -58,7 +58,8 @@ class ModeFilterSpec(Tidy3dBaseModel): sort_key: Optional[MODE_DATA_KEYS] = pd.Field( None, title="Sorting key", - description="Quantity used to sort modes within each filtered group.", + description="Quantity used to sort modes within each filtered group. If ``None``, " + "sorting is by descending effective index.", ) sort_reference: Optional[float] = pd.Field( None, @@ -73,6 +74,15 @@ class ModeFilterSpec(Tidy3dBaseModel): description="Sort order for the selected key or difference to reference value.", ) + # Frequency tracking - applied after sorting and filtering + track_freq: Optional[TrackFreq] = pd.Field( + "central", + title="Tracking base frequency", + description="If provided, enables cross-frequency mode tracking. Can be 'lowest', " + "'central', or 'highest'. The mode sorting would then be exact at the specified frequency, " + "while at other frequencies it can change depending on the mode tracking.", + ) + class ModeSpec(Tidy3dBaseModel): """ @@ -201,13 +211,10 @@ class ModeSpec(Tidy3dBaseModel): "Note: currently only supported when 'angle_phi' is a multiple of 'np.pi'.", ) - track_freq: Union[TrackFreq, None] = pd.Field( - "central", - title="Mode Tracking Frequency", - description="Parameter that turns on/off mode tracking based on their similarity. " - "Can take values ``'lowest'``, ``'central'``, or ``'highest'``, which correspond to " - "mode tracking based on the lowest, central, or highest frequency. " - "If ``None`` no mode tracking is performed.", + track_freq: Optional[TrackFreq] = pd.Field( + None, + title="Mode Tracking Frequency (deprecated)", + description="Deprecated. Use 'sort_spec.track_freq' instead.", ) group_index_step: Union[pd.PositiveFloat, bool] = pd.Field( @@ -219,13 +226,12 @@ class ModeSpec(Tidy3dBaseModel): f"default of {GROUP_INDEX_STEP} is used.", ) - filter_spec: Optional[ModeFilterSpec] = pd.Field( - None, + sort_spec: ModeSortSpec = pd.Field( + ModeSortSpec(), title="Mode filtering and sorting specification", - description="Defines how to filter and sort modes within each frequency. If ``None``, " - "sorting is by descending effective index (unless overridden by other options). If ``track_freq`` " - "is not ``None``, the sorting is only exact at the specified frequency, while at other frequencies " - "it can change depending on the mode tracking.", + description="Defines how to filter and sort modes within each frequency. If ``track_freq`` " + "is not ``None``, the sorting is only exact at the specified frequency, while at other " + "frequencies it can change depending on the mode tracking.", ) @pd.validator("bend_axis", always=True) @@ -274,10 +280,15 @@ def check_group_step_size(cls, val): def check_precision(cls, values): """Verify critical ModeSpec settings for group index calculation.""" if values["group_index_step"] > 0: - if values["track_freq"] is None: + # prefer explicit track_freq on ModeSpec, else fall back to sort_spec.track_freq + tf = values.get("track_freq") + if tf is None: + sort_spec = values.get("sort_spec") + tf = None if sort_spec is None else getattr(sort_spec, "track_freq", None) + if tf is None: log.warning( "Group index calculation without mode tracking can lead to incorrect results " - "around mode crossings. Consider setting 'track_freq' to 'central'." + "around mode crossings. Consider setting 'sort_spec.track_freq' to 'central'." ) # multiply by 5 to be safe @@ -303,17 +314,42 @@ def angle_rotation_with_phi(cls, val, values): @pd.root_validator(skip_on_failure=True) def _filter_pol_and_sort_spec_exclusive(cls, values): - """Ensure that 'filter_pol' and 'filter_spec' are not used together.""" - if values.get("filter_pol") is not None and values.get("filter_spec") is not None: - raise SetupError("'filter_pol' and 'filter_spec' cannot be used simultaneously.") + """Ensure that 'filter_pol' and 'sort_spec' are not used together.""" + sort_spec = values.get("sort_spec") + sort_or_filter = sort_spec.filter_key is not None or sort_spec.sort_key is not None + if values.get("filter_pol") is not None and sort_or_filter: + raise SetupError( + "'filter_pol' cannot be used simultaneously with sorting or filtering " + "defined in 'sort_spec'. Define the filtering in 'sort_spec' exclusively." + ) return values @pd.validator("filter_pol", always=True) def _filter_pol_deprecated(cls, val): - """Warn that 'filter_pol' is deprecated in favor of 'filter_spec'.""" + """Warn that 'filter_pol' is deprecated in favor of 'sort_spec'.""" if val is not None: log.warning( "'filter_pol' is deprecated and will be removed in future versions. " - "Please use 'filter_spec' instead." + "Please use 'sort_spec' instead." ) return val + + @pd.validator("track_freq", always=True) + def _track_freq_deprecated(cls, val): + """Warn that 'track_freq' on ModeSpec is deprecated in favor of 'sort_spec.track_freq'.""" + if val is not None: + log.warning( + "'ModeSpec.track_freq' is deprecated and will be removed in future versions. " + "Please use 'sort_spec.track_freq' instead." + ) + return val + + @property + def _track_freq(self) -> Optional[TrackFreq]: + """Private resolver for tracking frequency: prefers ModeSpec.track_freq if set, + otherwise falls back to ModeSortSpec.track_freq.""" + if self.track_freq is not None: + return self.track_freq + if self.sort_spec is not None: + return self.sort_spec.track_freq + return None From 97c0a4d591041f6f613fe62628318ef99776fee3 Mon Sep 17 00:00:00 2001 From: Momchil Minkov Date: Tue, 14 Oct 2025 15:46:01 +0200 Subject: [PATCH 7/9] Further refactors to unify and fix track_freq handling --- tests/test_data/test_data_arrays.py | 5 ++ tests/test_data/test_monitor_data.py | 11 ++- tests/test_plugins/test_mode_solver.py | 30 +++++-- tidy3d/__init__.py | 2 + tidy3d/components/data/monitor_data.py | 113 ++++++++++++------------ tidy3d/components/mode/data/sim_data.py | 11 +++ tidy3d/components/mode/mode_solver.py | 7 +- tidy3d/components/mode_spec.py | 3 +- 8 files changed, 111 insertions(+), 71 deletions(-) diff --git a/tests/test_data/test_data_arrays.py b/tests/test_data/test_data_arrays.py index 185ee4c2dc..f42af7d6b7 100644 --- a/tests/test_data/test_data_arrays.py +++ b/tests/test_data/test_data_arrays.py @@ -205,6 +205,11 @@ def make_mode_index_data_array(): return td.ModeIndexDataArray(values, coords={"f": FS, "mode_index": MODE_INDICES}) +def make_group_index_data_array(): + values = (1 + 0.1j) * np.random.random((len(FS), len(MODE_INDICES))) + return td.GroupIndexDataArray(values, coords={"f": FS, "mode_index": MODE_INDICES}) + + def make_far_field_data_array(): values = (1 + 1j) * np.random.random((len(PD), len(THETAS), len(PHIS), len(FS))) return td.FieldProjectionAngleDataArray( diff --git a/tests/test_data/test_monitor_data.py b/tests/test_data/test_monitor_data.py index b1abe24127..ab0598f8c0 100644 --- a/tests/test_data/test_monitor_data.py +++ b/tests/test_data/test_monitor_data.py @@ -50,6 +50,7 @@ make_far_field_data_array, make_flux_data_array, make_flux_time_data_array, + make_group_index_data_array, make_mode_amps_data_array, make_mode_index_data_array, make_scalar_field_data_array, @@ -61,6 +62,7 @@ # data array instances AMPS = make_mode_amps_data_array() N_COMPLEX = make_mode_index_data_array() +N_GROUP = make_group_index_data_array() FLUX = make_flux_data_array() FLUX_TIME = make_flux_time_data_array() GRID_CORRECTION = FreqModeDataArray( @@ -179,6 +181,7 @@ def make_mode_solver_data_smooth(conjugated_dot_product: bool = True): symmetry_center=SIM_SYM.center, grid_expanded=SIM_SYM.discretize_monitor(MODE_MONITOR_WITH_FIELDS), n_complex=N_COMPLEX.copy(), + n_group=N_GROUP.copy(), grid_primal_correction=GRID_CORRECTION, grid_dual_correction=GRID_CORRECTION, amps=AMPS.copy(), @@ -709,7 +712,6 @@ def test_mode_solver_data_sort(conjugated_dot_product): # make it unsorted num_modes = len(data.Ex.coords["mode_index"]) num_freqs = len(data.Ex.coords["f"]) - phases = 2 * np.pi * np.random.random((num_freqs, num_modes)) unsorting = np.arange(num_modes) * np.ones((num_freqs, num_modes)) unsorting = unsorting.astype(int) # we keep first, central, and last sorted @@ -718,7 +720,11 @@ def test_mode_solver_data_sort(conjugated_dot_product): unsorting[freq_id, :] = np.random.permutation(unsorting[freq_id, :]) # unsort using sorting tool - data_unsorted = data._reorder_modes(unsorting, phases, None) + data_unsorted = data._apply_mode_reorder(unsorting) + assert not np.allclose(data.n_complex, data_unsorted.n_complex) + assert not np.allclose(data.grid_dual_correction, data_unsorted.grid_dual_correction) + assert not np.allclose(data.grid_primal_correction, data_unsorted.grid_primal_correction) + assert not np.allclose(data.n_group, data_unsorted.n_group) # sort back using all starting frequencies overlap_thresh = 0.95 @@ -733,6 +739,7 @@ def test_mode_solver_data_sort(conjugated_dot_product): assert np.allclose(data.n_complex, data_sorted.n_complex) assert np.allclose(data.grid_dual_correction, data_sorted.grid_dual_correction) assert np.allclose(data.grid_primal_correction, data_sorted.grid_primal_correction) + assert np.allclose(data.n_group, data_sorted.n_group) # make sure neighboring frequencies are in phase data_1 = data._isel(f=[0]) diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 78d8151aca..09534e91e8 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -1321,10 +1321,11 @@ def test_modes_filter_sort(): boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), sources=[SRC], ) + # turn off track_freq so sorting is exact at all freqs mode_spec = td.ModeSpec( num_modes=5, target_neff=2.0, - sort_spec=td.ModeSortSpec(sort_key="n_eff", sort_order="ascending"), + sort_spec=td.ModeSortSpec(sort_key="n_eff", sort_order="ascending", track_freq=None), num_pml=(10, 10), ) ms = ModeSolver( @@ -1340,20 +1341,23 @@ def test_modes_filter_sort(): assert np.all(n_eff.diff(dim="mode_index") >= 0) for key in get_args(MODE_DATA_KEYS): + print(key) # Test ascending - sort_spec = td.ModeSortSpec(sort_key=key, sort_order="ascending") + sort_spec = td.ModeSortSpec(sort_key=key, sort_order="ascending", track_freq=None) modes = modes.sort_modes(sort_spec) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") >= 0) # Test descending - sort_spec = td.ModeSortSpec(sort_key=key, sort_order="descending") + sort_spec = td.ModeSortSpec(sort_key=key, sort_order="descending", track_freq=None) modes = modes.sort_modes(sort_spec) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") <= 0) # Test descending with a large reference value should be the same as ascending - sort_spec = td.ModeSortSpec(sort_key=key, sort_order="descending", sort_reference=100) + sort_spec = td.ModeSortSpec( + sort_key=key, sort_order="descending", sort_reference=100, track_freq=None + ) modes = modes.sort_modes(sort_spec) metric = getattr(modes, key) assert np.all(metric.diff(dim="mode_index") >= 0) @@ -1370,6 +1374,7 @@ def test_modes_filter_sort(): filter_order="over", sort_key="k_eff", sort_order="ascending", + track_freq=None, ) modes = modes.sort_modes(sort_spec) for ifreq in range(len(ms.freqs)): @@ -1391,7 +1396,7 @@ def test_modes_filter_sort(): metric_sorted.isel(mode_index=slice(first_group_size, None)).diff(dim="mode_index") >= 0 ) - # Test filter only with filter_order="under", and default sorting order + # Test filter only with filter_order="under", and no sorting defined ms = ms.updated_copy( mode_spec=mode_spec.updated_copy( sort_spec=td.ModeSortSpec( @@ -1401,8 +1406,8 @@ def test_modes_filter_sort(): ) ) ) - # need to solve again because the default only applies to the solver - # if the modes had been reordered previously, and sort_val is None, no sorting will happen + # need to solve again because so that the default sorting from the solver will apply, as + # the modes had been reordered previously, and sort_val is None modes = ms.solve() for ifreq in range(len(ms.freqs)): metric_filtered = modes.TE_fraction.isel(f=ifreq) @@ -1424,3 +1429,14 @@ def test_modes_filter_sort(): assert np.all( metric_sorted.isel(mode_index=slice(first_group_size, None)).diff(dim="mode_index") <= 0 ) + + # Test that if we now reorder based on a track_freq, the original order is preserved at + # the track_freq, but not at the other one + sort_spec = td.ModeSortSpec( + sort_key="k_eff", + sort_order="ascending", + track_freq="lowest", + ) + modes = modes.sort_modes(sort_spec=sort_spec) + assert np.all(np.diff(modes.k_eff.isel(f=0)) >= 0) + assert not np.all(np.diff(modes.k_eff.isel(f=-1)) >= 0) diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 37706f00a2..8d83379ee9 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -153,6 +153,7 @@ FieldProjectionKSpaceDataArray, FluxDataArray, FluxTimeDataArray, + GroupIndexDataArray, HeatDataArray, IndexedDataArray, IndexedFieldVoltageDataArray, @@ -590,6 +591,7 @@ def set_logging_level(level: str) -> None: "GridRefinementLine", "GridRefinementRegion", "GridSpec", + "GroupIndexDataArray", "HammerstadSurfaceRoughness", "HeatBoundarySpec", "HeatChargeBoundarySpec", diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index f8577b55f5..2bfbc84b8b 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -1759,13 +1759,22 @@ def overlap_sort( data_template = data_to_sort # Rearrange modes using computed sorting values - mode_data_sorted = self._reorder_modes( - sorting=sorting, - phase=phase, - track_freq=track_freq, - ) - return mode_data_sorted + # 1) Reorder using the shared implementation + data_reordered = self._apply_mode_reorder(sorting) + + # 2) Apply phase shifts to field components in-place (data_reordered is already a copy) + for field in data_reordered.field_components.values(): + phase_fact = np.exp(-1j * phase[None, None, None, :, :]).astype(field.data.dtype) + field.values *= phase_fact + + # 3) Update mode_spec: prefer sort_spec.track_freq; clear deprecated track_freq + mspec = data_reordered.monitor.mode_spec + sort_spec = mspec.sort_spec.updated_copy(track_freq=track_freq) + mspec_updated = mspec.updated_copy(sort_spec=sort_spec, track_freq=None) + monitor_updated = data_reordered.monitor.updated_copy(mode_spec=mspec_updated) + + return data_reordered.updated_copy(monitor=monitor_updated, deep=False, validate=False) def _isel(self, **isel_kwargs): """Wraps ``xarray.DataArray.isel`` for all data fields that are defined over frequency and @@ -1852,50 +1861,6 @@ def _find_closest_pairs(arr: Numpy) -> tuple[Numpy, Numpy]: return pairs, values - def _reorder_modes( - self, - sorting: Numpy, - phase: Numpy, - track_freq: TrackFreq, - ) -> ModeData: - """Rearrange modes for the i-th frequency according to sorting[i, :] and apply phase - shifts.""" - - num_freqs, _ = np.shape(sorting) - - # Create new dict with rearranged field components - update_dict = {} - for field_name, field in self.field_components.items(): - field_sorted = field.copy() - - # Rearrange modes - for freq_id in range(num_freqs): - field_sorted.data[..., freq_id, :] = field_sorted.data[ - ..., freq_id, sorting[freq_id, :] - ] - - # Apply phase shift - phase_fact = np.exp(-1j * phase[None, None, None, :, :]).astype(field_sorted.data.dtype) - field_sorted.data = field_sorted.data * phase_fact - - update_dict[field_name] = field_sorted - - # Rearrange data over f and mode_index - data_dict = dict(**self._grid_correction_dict, n_complex=self.n_complex) - for key, data in data_dict.items(): - update_dict[key] = data.copy() - for freq_id in range(num_freqs): - update_dict[key].data[freq_id, :] = update_dict[key].data[ - freq_id, sorting[freq_id, :] - ] - - # Update mode_spec in the monitor; set deprecated track_freq to None - mode_spec = self.monitor.mode_spec.updated_copy(path="sort_spec", track_freq=track_freq) - mode_spec = self.monitor.mode_spec.updated_copy(track_freq=None) - update_dict["monitor"] = self.monitor.copy(update={"mode_spec": mode_spec}) - - return self.copy(update=update_dict) - def _group_index_post_process(self, frequency_step: float) -> ModeData: """Calculate group index and remove added frequencies used only for this calculation. @@ -2258,12 +2223,34 @@ def _apply_mode_reorder(self, sort_inds_2d): return self.updated_copy(**modify_data) def sort_modes(self, sort_spec: Optional[ModeSortSpec] = None) -> ModeSolverData: - """Sort modes per frequency according to ModeSpec.sort_spec. + """Sort modes per frequency according to ``sort_spec``. + + The modes are first filtered if ``sort_spec.filter_key`` is provided. They are then sorted + within each filtered group according to ``sort_spec.sort_key``. if provided. Finally, + if a tracking frequency is also provided, the tracking is applied. The tracking could + reshuffle the filter/sort criteria at frequencies away from the tracking frequency. - Returns the (possibly reordered) mode_solver_data. + Note + ---- + If the deprecated ``self.monitor.mode_spec.track_freq`` is set, it will still take + precedence over ``sort_spec.track_freq``. A warning will be logged in this case. + + Parameters + ---------- + sort_spec : Optional[:class:`.ModeSortSpec`] + Specification of how to sort the modes. + + + Returns + ------- + :class:`.ModeSolverData` + Copy of self with modes sorted according to ``sort_spec``. """ - # Always return the original data if no filter spec - if sort_spec is None: + + mode_spec_orig = self.monitor.mode_spec + + # Always return the original data if no sort spec or modes already appropriately sorted + if sort_spec is None: # or sort_spec == mode_spec_orig.sort_spec: return self num_freqs = self.n_eff["f"].size @@ -2324,8 +2311,22 @@ def _order_indices(indices, vals_all): return self data_sorted = self._apply_mode_reorder(sort_inds_2d) - monitor_updated = data_sorted.monitor.updated_copy(path="mode_spec", sort_spec=sort_spec) - return data_sorted._updated({"monitor": monitor_updated}) + mode_spec_updated = mode_spec_orig.updated_copy(sort_spec=sort_spec) + + # sort modes across frequencies if requested + track_freq = mode_spec_updated._track_freq + if track_freq and num_freqs > 1: + if track_freq != sort_spec.track_freq: + # If ``track_freq`` was defined on the 'mode_spec' level, we still need to use it. + # Log a warning for the user as it would take precedence over ``sort_spec.track_freq``. + log.warning( + "Ignoring provided 'track_freq' parameter in 'mode_spec.sort_spec'. Using the " + f"deprecated, but defined, 'mode_spec.track_freq' = '{track_freq}' instead." + ) + data_sorted = data_sorted.overlap_sort(track_freq) + + monitor_updated = data_sorted.monitor.updated_copy(mode_spec=mode_spec_updated) + return data_sorted.updated_copy(monitor=monitor_updated, deep=False, validate=False) class ModeSolverData(ModeData): diff --git a/tidy3d/components/mode/data/sim_data.py b/tidy3d/components/mode/data/sim_data.py index 6f996cfff7..8ae9eeaf1d 100644 --- a/tidy3d/components/mode/data/sim_data.py +++ b/tidy3d/components/mode/data/sim_data.py @@ -10,6 +10,7 @@ from tidy3d.components.data.monitor_data import MediumData, ModeSolverData, PermittivityData from tidy3d.components.data.sim_data import AbstractYeeGridSimulationData from tidy3d.components.mode.simulation import ModeSimulation +from tidy3d.components.mode_spec import ModeSortSpec from tidy3d.components.types import Ax, PlotScale ModeSimulationMonitorDataType = Union[PermittivityData, MediumData] @@ -101,3 +102,13 @@ def plot_field( ax=ax, **sel_kwargs, ) + + def sort_modes(self, sort_spec: Optional[ModeSortSpec] = None) -> ModeSimulationData: + """Sort modes per frequency according to ``sort_spec``.""" + + if sort_spec is None: + return self + + modes_sorted = self.modes_raw.sort_modes(sort_spec=sort_spec) + monitor_updated = modes_sorted.monitor.updated_copy(path="mode_spec", sort_spec=sort_spec) + return modes_sorted._updated({"monitor": monitor_updated}) diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index ccc6cb2daf..40b9979c91 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -528,11 +528,6 @@ def data_raw(self) -> ModeSolverData: # filter and sort modes if requested by sort_spec mode_solver_data = mode_solver_data.sort_modes(self.mode_spec.sort_spec) - # sort modes across frequencies if requested - track_freq = self.mode_spec._track_freq - if track_freq and len(self.freqs) > 1: - mode_solver_data = mode_solver_data.overlap_sort(track_freq) - self._field_decay_warning(mode_solver_data.symmetry_expanded) mode_solver_data = self._filter_components(mode_solver_data) @@ -602,6 +597,8 @@ def rotated_mode_solver_data(self) -> ModeSolverData: # Make mode solver data on the Yee grid mode_solver_monitor = solver.to_mode_solver_monitor(name=MODE_MONITOR_NAME) + # Modes are not yet sorted based on the sort_spec + mode_solver_monitor = mode_solver_monitor.updated_copy(path="mode_spec", sort_spec=None) grid_expanded = solver.simulation.discretize_monitor(mode_solver_monitor) rotated_mode_data = ModeSolverData( monitor=mode_solver_monitor, diff --git a/tidy3d/components/mode_spec.py b/tidy3d/components/mode_spec.py index 449079a551..8454af6ddf 100644 --- a/tidy3d/components/mode_spec.py +++ b/tidy3d/components/mode_spec.py @@ -79,7 +79,8 @@ class ModeSortSpec(Tidy3dBaseModel): "central", title="Tracking base frequency", description="If provided, enables cross-frequency mode tracking. Can be 'lowest', " - "'central', or 'highest'. The mode sorting would then be exact at the specified frequency, " + "'central', or 'highest', which refers to the frequency **index** in the list of " + "frequencies. The mode sorting would then be exact at the specified frequency, " "while at other frequencies it can change depending on the mode tracking.", ) From 992b3e054401b5aae10751de09bb3be97a9f62ab Mon Sep 17 00:00:00 2001 From: Momchil Minkov Date: Wed, 15 Oct 2025 12:25:26 +0200 Subject: [PATCH 8/9] Final reorg --- tests/test_components/test_mode.py | 6 +++ tests/test_plugins/test_mode_solver.py | 63 +++++++++++++++++++++++ tidy3d/components/base.py | 4 ++ tidy3d/components/data/monitor_data.py | 66 +++++++++++++------------ tidy3d/components/mode/data/sim_data.py | 11 ++--- tidy3d/components/mode/mode_solver.py | 5 +- 6 files changed, 117 insertions(+), 38 deletions(-) diff --git a/tests/test_components/test_mode.py b/tests/test_components/test_mode.py index b3dfbda21f..c8f92096d7 100644 --- a/tests/test_components/test_mode.py +++ b/tests/test_components/test_mode.py @@ -338,6 +338,12 @@ def test_mode_sim_data(): sim_data = get_mode_sim_data() _ = sim_data.plot_field("Ey", ax=AX, mode_index=0, f=FS[0]) + sort_spec = td.ModeSortSpec(sort_key="k_eff", track_freq=None) + sim_data_sorted = sim_data.sort_modes(sort_spec) + assert sim_data_sorted.simulation.mode_spec.sort_spec == sort_spec + assert sim_data_sorted.modes_raw.monitor.mode_spec.sort_spec == sort_spec + assert np.all(sim_data_sorted.modes_raw.k_eff.diff(dim="mode_index") >= 0) + def test_plane_crosses_symmetry_plane_warning(monkeypatch): """Test that a warning is issued if the mode plane crosses a symmetry plane but the centers do not match.""" diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 09534e91e8..97155fdd56 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -1440,3 +1440,66 @@ def test_modes_filter_sort(): modes = modes.sort_modes(sort_spec=sort_spec) assert np.all(np.diff(modes.k_eff.isel(f=0)) >= 0) assert not np.all(np.diff(modes.k_eff.isel(f=-1)) >= 0) + + +def test_sort_spec_track_freq(): + """Test various ways to sort and track that should result in the same final modes.""" + simulation = td.Simulation( + size=SIM_SIZE, + grid_spec=td.GridSpec(wavelength=1.0), + structures=[WAVEGUIDE], + run_time=1e-12, + symmetry=(0, 0, 1), + boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), + sources=[SRC], + ) + sort_spec = td.ModeSortSpec(sort_key="TE_fraction") + mode_spec = td.ModeSpec( + num_modes=5, + target_neff=2.0, + sort_spec=sort_spec.updated_copy(track_freq="lowest"), + num_pml=(10, 10), + group_index_step=True, + ) + ms = ModeSolver( + simulation=simulation, + plane=PLANE, + mode_spec=mode_spec, + freqs=[td.C_0 / 0.5, td.C_0 / 1.0, td.C_0 / 2.0], + direction="-", + ) + modes_lowest = ms.solve() + + # TODO remove this when track_freq is removed + mode_spec = td.ModeSpec( + num_modes=5, + target_neff=2.0, + sort_spec=sort_spec, + track_freq="lowest", + num_pml=(10, 10), + group_index_step=True, + ) + ms = ms.updated_copy(mode_spec=mode_spec) + modes_lowest_legacy = ms.solve() + + assert modes_lowest == modes_lowest_legacy + + mode_spec = td.ModeSpec( + num_modes=5, + target_neff=2.0, + sort_spec=sort_spec.updated_copy(track_freq=None), + num_pml=(10, 10), + group_index_step=True, + ) + ms = ms.updated_copy(mode_spec=mode_spec) + modes_untracked = ms.solve() + + assert not np.all(modes_lowest.n_eff == modes_untracked.n_eff) + + modes_lowest_retracked = modes_untracked.overlap_sort(track_freq="lowest") + + # The field modes come out with a different phase so the datas are not equivalent but we can + # check that everything matches + assert np.allclose(modes_lowest.Ex.abs, modes_lowest_retracked.Ex.abs) + assert np.all(modes_lowest.n_eff == modes_lowest_retracked.n_eff) + assert np.all(modes_lowest.n_group == modes_lowest_retracked.n_group) diff --git a/tidy3d/components/base.py b/tidy3d/components/base.py index 2a51c3bd2e..29a674d2cd 100644 --- a/tidy3d/components/base.py +++ b/tidy3d/components/base.py @@ -960,6 +960,7 @@ def check_equal(dict1: dict, dict2: dict) -> bool: # if one of val1 or val2 is None (exclusive OR) if (val1 is None) != (val2 is None): + print(key) return False # convert tuple to dict to use this recursive function @@ -971,17 +972,20 @@ def check_equal(dict1: dict, dict2: dict) -> bool: if isinstance(val1, dict) or isinstance(val2, dict): are_equal = check_equal(val1, val2) if not are_equal: + print(key) return False # if numpy arrays, use numpy to do equality check elif isinstance(val1, np.ndarray) or isinstance(val2, np.ndarray): if not np.array_equal(val1, val2): + print(key) return False # everything else else: # note: this logic is because != is handled differently in DataArrays apparently if not val1 == val2: + print(key) return False return True diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index 2bfbc84b8b..8a7fd60eeb 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -1680,6 +1680,12 @@ def overlap_sort( corresponds to physically the same mode at all frequencies. Modes with overlap values over ``overlap_thresh`` are considered matching and not rearranged. + Note + ---- + The monitor associated to this data is updated so that the deprecated + ``monitor.mode_spec.track_freq`` is set to ``None``, while + ``monitor.mode_spec.sort_spec.track_freq`` is set to the provided ``track_freq``. + Parameters ---------- track_freq : Literal["central", "lowest", "highest"] @@ -1760,7 +1766,7 @@ def overlap_sort( # Rearrange modes using computed sorting values - # 1) Reorder using the shared implementation + # 1) Reorder using the shared implementation (creates a copy) data_reordered = self._apply_mode_reorder(sorting) # 2) Apply phase shifts to field components in-place (data_reordered is already a copy) @@ -1771,8 +1777,10 @@ def overlap_sort( # 3) Update mode_spec: prefer sort_spec.track_freq; clear deprecated track_freq mspec = data_reordered.monitor.mode_spec sort_spec = mspec.sort_spec.updated_copy(track_freq=track_freq) - mspec_updated = mspec.updated_copy(sort_spec=sort_spec, track_freq=None) - monitor_updated = data_reordered.monitor.updated_copy(mode_spec=mspec_updated) + mspec_updated = mspec.updated_copy(sort_spec=sort_spec, track_freq=None, validate=False) + monitor_updated = data_reordered.monitor.updated_copy( + mode_spec=mspec_updated, validate=False + ) return data_reordered.updated_copy(monitor=monitor_updated, deep=False, validate=False) @@ -2222,24 +2230,26 @@ def _apply_mode_reorder(self, sort_inds_2d): modify_data[key] = data return self.updated_copy(**modify_data) - def sort_modes(self, sort_spec: Optional[ModeSortSpec] = None) -> ModeSolverData: + def sort_modes( + self, sort_spec: Optional[ModeSortSpec] = None, track_freq: Optional[str] = None + ) -> ModeSolverData: """Sort modes per frequency according to ``sort_spec``. The modes are first filtered if ``sort_spec.filter_key`` is provided. They are then sorted within each filtered group according to ``sort_spec.sort_key``. if provided. Finally, - if a tracking frequency is also provided, the tracking is applied. The tracking could - reshuffle the filter/sort criteria at frequencies away from the tracking frequency. - - Note - ---- - If the deprecated ``self.monitor.mode_spec.track_freq`` is set, it will still take - precedence over ``sort_spec.track_freq``. A warning will be logged in this case. + if a tracking frequency is also provided either in ``sort_spec`` or as a separate argument, + the tracking is applied . The tracking could reshuffle the filter/sort criteria at + frequencies away from the tracking frequency. Parameters ---------- sort_spec : Optional[:class:`.ModeSortSpec`] Specification of how to sort the modes. - + track_freq : Optional[Literal["central", "lowest", "highest"]] + Specifies that modes should be tracked across frequencies. Overrides + ``sort_spec.track_freq``, but the returned data will have + ``monitor.mode_spec.sort_spec.track_freq`` set to the provided value, while + ``self.monitor.mode_spec.track_freq`` will be set to ``None``. Returns ------- @@ -2247,10 +2257,8 @@ def sort_modes(self, sort_spec: Optional[ModeSortSpec] = None) -> ModeSolverData Copy of self with modes sorted according to ``sort_spec``. """ - mode_spec_orig = self.monitor.mode_spec - - # Always return the original data if no sort spec or modes already appropriately sorted - if sort_spec is None: # or sort_spec == mode_spec_orig.sort_spec: + # Return the original data if no new sorting / tracking required + if track_freq is None and sort_spec is None: return self num_freqs = self.n_eff["f"].size @@ -2308,25 +2316,21 @@ def _order_indices(indices, vals_all): # If all rows are identity, skip if np.all(sort_inds_2d == np.tile(identity, (num_freqs, 1))): - return self - - data_sorted = self._apply_mode_reorder(sort_inds_2d) - mode_spec_updated = mode_spec_orig.updated_copy(sort_spec=sort_spec) + data_sorted = self + else: + data_sorted = self._apply_mode_reorder(sort_inds_2d) # this creates a copy + data_sorted = data_sorted.updated_copy( + path="monitor/mode_spec", sort_spec=sort_spec, deep=False, validate=False + ) - # sort modes across frequencies if requested - track_freq = mode_spec_updated._track_freq + # Sort modes across frequencies if requested. + # Note: after sorting, ``track_freq`` is set in ``sort_spec`` regardless of how it was + # provided. The deprecated ``mode_spec.track_freq`` is cleared. + track_freq = track_freq or sort_spec.track_freq if track_freq and num_freqs > 1: - if track_freq != sort_spec.track_freq: - # If ``track_freq`` was defined on the 'mode_spec' level, we still need to use it. - # Log a warning for the user as it would take precedence over ``sort_spec.track_freq``. - log.warning( - "Ignoring provided 'track_freq' parameter in 'mode_spec.sort_spec'. Using the " - f"deprecated, but defined, 'mode_spec.track_freq' = '{track_freq}' instead." - ) data_sorted = data_sorted.overlap_sort(track_freq) - monitor_updated = data_sorted.monitor.updated_copy(mode_spec=mode_spec_updated) - return data_sorted.updated_copy(monitor=monitor_updated, deep=False, validate=False) + return data_sorted class ModeSolverData(ModeData): diff --git a/tidy3d/components/mode/data/sim_data.py b/tidy3d/components/mode/data/sim_data.py index 8ae9eeaf1d..f46e24af6c 100644 --- a/tidy3d/components/mode/data/sim_data.py +++ b/tidy3d/components/mode/data/sim_data.py @@ -103,12 +103,11 @@ def plot_field( **sel_kwargs, ) - def sort_modes(self, sort_spec: Optional[ModeSortSpec] = None) -> ModeSimulationData: + def sort_modes(self, sort_spec: ModeSortSpec) -> ModeSimulationData: """Sort modes per frequency according to ``sort_spec``.""" - if sort_spec is None: - return self - modes_sorted = self.modes_raw.sort_modes(sort_spec=sort_spec) - monitor_updated = modes_sorted.monitor.updated_copy(path="mode_spec", sort_spec=sort_spec) - return modes_sorted._updated({"monitor": monitor_updated}) + data_sorted = self.updated_copy(modes_raw=modes_sorted) + return data_sorted.updated_copy( + path="simulation", mode_spec=modes_sorted.monitor.mode_spec, deep=False, validate=False + ) diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index 40b9979c91..9ba51676ef 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -526,7 +526,10 @@ def data_raw(self) -> ModeSolverData: mode_solver_data = self._filter_polarization(mode_solver_data=mode_solver_data) # filter and sort modes if requested by sort_spec - mode_solver_data = mode_solver_data.sort_modes(self.mode_spec.sort_spec) + mode_solver_data = mode_solver_data.sort_modes( + sort_spec=self.mode_spec.sort_spec, + track_freq=self.mode_spec.track_freq, + ) self._field_decay_warning(mode_solver_data.symmetry_expanded) From 66d06364b7652a37cde088d8af36024d4de3a951 Mon Sep 17 00:00:00 2001 From: Momchil Minkov Date: Wed, 15 Oct 2025 12:25:46 +0200 Subject: [PATCH 9/9] schemas --- schemas/EMESimulation.json | 216 +++++++++++++++--------- schemas/ModeSimulation.json | 219 +++++++++++++++--------- schemas/Simulation.json | 219 +++++++++++++++--------- schemas/TerminalComponentModeler.json | 233 +++++++++++++++++--------- 4 files changed, 562 insertions(+), 325 deletions(-) diff --git a/schemas/EMESimulation.json b/schemas/EMESimulation.json index ffd994db36..d9f69bddb2 100644 --- a/schemas/EMESimulation.json +++ b/schemas/EMESimulation.json @@ -4786,13 +4786,6 @@ ], "type": "string" }, - "filter_spec": { - "allOf": [ - { - "$ref": "#/definitions/ModeFilterSpec" - } - ] - }, "group_index_step": { "anyOf": [ { @@ -4838,6 +4831,24 @@ ], "type": "string" }, + "sort_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeSortSpec" + } + ], + "default": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + } + }, "target_neff": { "exclusiveMinimum": 0, "type": "number" @@ -7491,7 +7502,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -7499,8 +7509,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -7527,70 +7548,6 @@ ], "type": "object" }, - "ModeFilterSpec": { - "additionalProperties": false, - "properties": { - "attrs": { - "default": {}, - "type": "object" - }, - "filter_key": { - "enum": [ - "TE_fraction", - "TM_fraction", - "k_eff", - "mode_area", - "n_eff", - "wg_TE_fraction", - "wg_TM_fraction" - ], - "type": "string" - }, - "filter_order": { - "default": "over", - "enum": [ - "over", - "under" - ], - "type": "string" - }, - "filter_reference": { - "default": 0.0, - "type": "number" - }, - "sort_key": { - "enum": [ - "TE_fraction", - "TM_fraction", - "k_eff", - "mode_area", - "n_eff", - "wg_TE_fraction", - "wg_TM_fraction" - ], - "type": "string" - }, - "sort_order": { - "default": "descending", - "enum": [ - "ascending", - "descending" - ], - "type": "string" - }, - "sort_reference": { - "type": "number" - }, - "type": { - "default": "ModeFilterSpec", - "enum": [ - "ModeFilterSpec" - ], - "type": "string" - } - }, - "type": "object" - }, "ModeSolverMonitor": { "additionalProperties": false, "properties": { @@ -7756,7 +7713,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -7764,8 +7720,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -7842,6 +7809,79 @@ ], "type": "object" }, + "ModeSortSpec": { + "additionalProperties": false, + "properties": { + "attrs": { + "default": {}, + "type": "object" + }, + "filter_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "filter_order": { + "default": "over", + "enum": [ + "over", + "under" + ], + "type": "string" + }, + "filter_reference": { + "default": 0.0, + "type": "number" + }, + "sort_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "sort_order": { + "default": "ascending", + "enum": [ + "ascending", + "descending" + ], + "type": "string" + }, + "sort_reference": { + "type": "number" + }, + "track_freq": { + "default": "central", + "enum": [ + "central", + "highest", + "lowest" + ], + "type": "string" + }, + "type": { + "default": "ModeSortSpec", + "enum": [ + "ModeSortSpec" + ], + "type": "string" + } + }, + "type": "object" + }, "ModeSpec": { "additionalProperties": false, "properties": { @@ -7878,13 +7918,6 @@ ], "type": "string" }, - "filter_spec": { - "allOf": [ - { - "$ref": "#/definitions/ModeFilterSpec" - } - ] - }, "group_index_step": { "anyOf": [ { @@ -7930,12 +7963,29 @@ ], "type": "string" }, + "sort_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeSortSpec" + } + ], + "default": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + } + }, "target_neff": { "exclusiveMinimum": 0, "type": "number" }, "track_freq": { - "default": "central", "enum": [ "central", "highest", diff --git a/schemas/ModeSimulation.json b/schemas/ModeSimulation.json index c8273c10d6..efcba7e51e 100644 --- a/schemas/ModeSimulation.json +++ b/schemas/ModeSimulation.json @@ -6715,7 +6715,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -6723,8 +6722,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -6751,70 +6761,6 @@ ], "type": "object" }, - "ModeFilterSpec": { - "additionalProperties": false, - "properties": { - "attrs": { - "default": {}, - "type": "object" - }, - "filter_key": { - "enum": [ - "TE_fraction", - "TM_fraction", - "k_eff", - "mode_area", - "n_eff", - "wg_TE_fraction", - "wg_TM_fraction" - ], - "type": "string" - }, - "filter_order": { - "default": "over", - "enum": [ - "over", - "under" - ], - "type": "string" - }, - "filter_reference": { - "default": 0.0, - "type": "number" - }, - "sort_key": { - "enum": [ - "TE_fraction", - "TM_fraction", - "k_eff", - "mode_area", - "n_eff", - "wg_TE_fraction", - "wg_TM_fraction" - ], - "type": "string" - }, - "sort_order": { - "default": "descending", - "enum": [ - "ascending", - "descending" - ], - "type": "string" - }, - "sort_reference": { - "type": "number" - }, - "type": { - "default": "ModeFilterSpec", - "enum": [ - "ModeFilterSpec" - ], - "type": "string" - } - }, - "type": "object" - }, "ModeMonitor": { "additionalProperties": false, "properties": { @@ -6950,7 +6896,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -6958,8 +6903,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -7201,7 +7157,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -7209,8 +7164,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -7287,6 +7253,79 @@ ], "type": "object" }, + "ModeSortSpec": { + "additionalProperties": false, + "properties": { + "attrs": { + "default": {}, + "type": "object" + }, + "filter_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "filter_order": { + "default": "over", + "enum": [ + "over", + "under" + ], + "type": "string" + }, + "filter_reference": { + "default": 0.0, + "type": "number" + }, + "sort_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "sort_order": { + "default": "ascending", + "enum": [ + "ascending", + "descending" + ], + "type": "string" + }, + "sort_reference": { + "type": "number" + }, + "track_freq": { + "default": "central", + "enum": [ + "central", + "highest", + "lowest" + ], + "type": "string" + }, + "type": { + "default": "ModeSortSpec", + "enum": [ + "ModeSortSpec" + ], + "type": "string" + } + }, + "type": "object" + }, "ModeSource": { "additionalProperties": false, "properties": { @@ -7376,7 +7415,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -7384,8 +7422,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -7517,13 +7566,6 @@ ], "type": "string" }, - "filter_spec": { - "allOf": [ - { - "$ref": "#/definitions/ModeFilterSpec" - } - ] - }, "group_index_step": { "anyOf": [ { @@ -7569,12 +7611,29 @@ ], "type": "string" }, + "sort_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeSortSpec" + } + ], + "default": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + } + }, "target_neff": { "exclusiveMinimum": 0, "type": "number" }, "track_freq": { - "default": "central", "enum": [ "central", "highest", diff --git a/schemas/Simulation.json b/schemas/Simulation.json index dde5295279..74e3c2dde8 100644 --- a/schemas/Simulation.json +++ b/schemas/Simulation.json @@ -10199,7 +10199,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10207,8 +10206,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -10235,70 +10245,6 @@ ], "type": "object" }, - "ModeFilterSpec": { - "additionalProperties": false, - "properties": { - "attrs": { - "default": {}, - "type": "object" - }, - "filter_key": { - "enum": [ - "TE_fraction", - "TM_fraction", - "k_eff", - "mode_area", - "n_eff", - "wg_TE_fraction", - "wg_TM_fraction" - ], - "type": "string" - }, - "filter_order": { - "default": "over", - "enum": [ - "over", - "under" - ], - "type": "string" - }, - "filter_reference": { - "default": 0.0, - "type": "number" - }, - "sort_key": { - "enum": [ - "TE_fraction", - "TM_fraction", - "k_eff", - "mode_area", - "n_eff", - "wg_TE_fraction", - "wg_TM_fraction" - ], - "type": "string" - }, - "sort_order": { - "default": "descending", - "enum": [ - "ascending", - "descending" - ], - "type": "string" - }, - "sort_reference": { - "type": "number" - }, - "type": { - "default": "ModeFilterSpec", - "enum": [ - "ModeFilterSpec" - ], - "type": "string" - } - }, - "type": "object" - }, "ModeMonitor": { "additionalProperties": false, "properties": { @@ -10434,7 +10380,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10442,8 +10387,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -10685,7 +10641,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10693,8 +10648,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -10771,6 +10737,79 @@ ], "type": "object" }, + "ModeSortSpec": { + "additionalProperties": false, + "properties": { + "attrs": { + "default": {}, + "type": "object" + }, + "filter_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "filter_order": { + "default": "over", + "enum": [ + "over", + "under" + ], + "type": "string" + }, + "filter_reference": { + "default": 0.0, + "type": "number" + }, + "sort_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "sort_order": { + "default": "ascending", + "enum": [ + "ascending", + "descending" + ], + "type": "string" + }, + "sort_reference": { + "type": "number" + }, + "track_freq": { + "default": "central", + "enum": [ + "central", + "highest", + "lowest" + ], + "type": "string" + }, + "type": { + "default": "ModeSortSpec", + "enum": [ + "ModeSortSpec" + ], + "type": "string" + } + }, + "type": "object" + }, "ModeSource": { "additionalProperties": false, "properties": { @@ -10860,7 +10899,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10868,8 +10906,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -11001,13 +11050,6 @@ ], "type": "string" }, - "filter_spec": { - "allOf": [ - { - "$ref": "#/definitions/ModeFilterSpec" - } - ] - }, "group_index_step": { "anyOf": [ { @@ -11053,12 +11095,29 @@ ], "type": "string" }, + "sort_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeSortSpec" + } + ], + "default": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + } + }, "target_neff": { "exclusiveMinimum": 0, "type": "number" }, "track_freq": { - "default": "central", "enum": [ "central", "highest", diff --git a/schemas/TerminalComponentModeler.json b/schemas/TerminalComponentModeler.json index 95b55a10f4..11a720c590 100644 --- a/schemas/TerminalComponentModeler.json +++ b/schemas/TerminalComponentModeler.json @@ -10682,7 +10682,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10690,8 +10689,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -10718,70 +10728,6 @@ ], "type": "object" }, - "ModeFilterSpec": { - "additionalProperties": false, - "properties": { - "attrs": { - "default": {}, - "type": "object" - }, - "filter_key": { - "enum": [ - "TE_fraction", - "TM_fraction", - "k_eff", - "mode_area", - "n_eff", - "wg_TE_fraction", - "wg_TM_fraction" - ], - "type": "string" - }, - "filter_order": { - "default": "over", - "enum": [ - "over", - "under" - ], - "type": "string" - }, - "filter_reference": { - "default": 0.0, - "type": "number" - }, - "sort_key": { - "enum": [ - "TE_fraction", - "TM_fraction", - "k_eff", - "mode_area", - "n_eff", - "wg_TE_fraction", - "wg_TM_fraction" - ], - "type": "string" - }, - "sort_order": { - "default": "descending", - "enum": [ - "ascending", - "descending" - ], - "type": "string" - }, - "sort_reference": { - "type": "number" - }, - "type": { - "default": "ModeFilterSpec", - "enum": [ - "ModeFilterSpec" - ], - "type": "string" - } - }, - "type": "object" - }, "ModeMonitor": { "additionalProperties": false, "properties": { @@ -10917,7 +10863,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -10925,8 +10870,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -11168,7 +11124,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -11176,8 +11131,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -11254,6 +11220,79 @@ ], "type": "object" }, + "ModeSortSpec": { + "additionalProperties": false, + "properties": { + "attrs": { + "default": {}, + "type": "object" + }, + "filter_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "filter_order": { + "default": "over", + "enum": [ + "over", + "under" + ], + "type": "string" + }, + "filter_reference": { + "default": 0.0, + "type": "number" + }, + "sort_key": { + "enum": [ + "TE_fraction", + "TM_fraction", + "k_eff", + "mode_area", + "n_eff", + "wg_TE_fraction", + "wg_TM_fraction" + ], + "type": "string" + }, + "sort_order": { + "default": "ascending", + "enum": [ + "ascending", + "descending" + ], + "type": "string" + }, + "sort_reference": { + "type": "number" + }, + "track_freq": { + "default": "central", + "enum": [ + "central", + "highest", + "lowest" + ], + "type": "string" + }, + "type": { + "default": "ModeSortSpec", + "enum": [ + "ModeSortSpec" + ], + "type": "string" + } + }, + "type": "object" + }, "ModeSource": { "additionalProperties": false, "properties": { @@ -11343,7 +11382,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -11351,8 +11389,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } }, @@ -11484,13 +11533,6 @@ ], "type": "string" }, - "filter_spec": { - "allOf": [ - { - "$ref": "#/definitions/ModeFilterSpec" - } - ] - }, "group_index_step": { "anyOf": [ { @@ -11536,12 +11578,29 @@ ], "type": "string" }, + "sort_spec": { + "allOf": [ + { + "$ref": "#/definitions/ModeSortSpec" + } + ], + "default": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + } + }, "target_neff": { "exclusiveMinimum": 0, "type": "number" }, "track_freq": { - "default": "central", "enum": [ "central", "highest", @@ -16822,7 +16881,6 @@ "bend_axis": null, "bend_radius": null, "filter_pol": null, - "filter_spec": null, "group_index_step": false, "num_modes": 1, "num_pml": [ @@ -16830,8 +16888,19 @@ 0 ], "precision": "double", + "sort_spec": { + "attrs": {}, + "filter_key": null, + "filter_order": "over", + "filter_reference": 0.0, + "sort_key": null, + "sort_order": "ascending", + "sort_reference": null, + "track_freq": "central", + "type": "ModeSortSpec" + }, "target_neff": null, - "track_freq": "central", + "track_freq": null, "type": "ModeSpec" } },