-
Notifications
You must be signed in to change notification settings - Fork 66
FXC-3547-Adding mode sorting to ModeSpec #2869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5 files reviewed, 1 comment
861448e
to
e6fbfa1
Compare
Diff CoverageDiff: origin/develop...HEAD, staged and unstaged changes
Summary
tidy3d/components/mode/mode_solver.py
|
Actually @caseyflex @yuanshen-flexcompute hold off the review, I'm going to generalize this further to include filtering. |
bd946f2
to
609cfba
Compare
Okay now you can review. :) I've introduced advanced filter/sort options. You can select to filter by a given property so that values e.g. over or under a reference value will be put first in the list; then separately you can sort each of the two groups by some other criterion. I think with this #1391 can be closed - probably one thing that still would be nice to add though is some built-in that computes the field intensity close to the plane boundaries (maybe not just on them, but within some region), which can help with PML mode filtering. Or similarly some metric that computes the field intensity within a given region. |
609cfba
to
e0f0136
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Generally very clean and easy to use. I have some comments:
sort_order
should default toascending
(currentlydescending
) whensort_reference
is notNone
. This way, values closest to the reference are returned first.Sorting after filtering doesn't seem to work correctly in some cases. I tested filtering byThis actually might be user error. Things look good for now.mode_area
under a certain value, then sorting byn_eff
closest to a reference value. This resulted in the sorting "breaking" the filtered order in some cases. I have a test notebook if you would like to take a closer look.
Thanks!
Thanks!
I think that makes sense. Originally I set it to
Yeah, thanks, a minimally reproducible example would be great! I added a test for that but it might not be comprehensive enough so this will help fix and extend testing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great addition! It mostly looks good, just a few comments
mode_solver_data.grid_dual_correction, | ||
]: | ||
if hasattr(data, "values") and data.values.ndim >= 2: | ||
data.values[..., ifreq, :] = data.values[..., ifreq, sort_inds] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two comments here:
indexing xarray values directly is not robust compared to something like this: data.loc[dict(frequency=ifreq)] = data.sel(frequency=ifreq).isel(mode=sort_inds)
so that might be preferred, unless the performance is a big issue
also, maybe we can be clear that we mutate the provided mode_solver_data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So in general I agree with you but it turns out it's hard to mutate an xarray array in place. This
data.loc[{"f": freq}] = data.sel(f=freq).isel(mode_index=sort_inds)
fails with IndexError: dimension coordinate 'mode_index' conflicts between indexed and indexing objects
. I tinkered around with it a bit and it seems like the only way (or at least the main way) would be to reassign a data array rather than sort it in-place. Then this is complicated further by the fact that we need to modify multiple data arrays, so I think it might have to go through a dict representation and then update the mode data with the updated dict.
Overall it seems a bit too much given that the current approach works... Btw note that I just separated it into a function, otherwise it's the same sorting approach we've been using forever in filter_pol
.
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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have a public function sort_modes(mode_solver_data, filter_spec)
? That way we can apply it automatically, or the user can do additional filtering as a post-processing step?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I was thinking about this myself and I was just worried about some potential issues / confusion, but I think after thinking about it more carefully now it could be worth it. I think the method could be a call on ModeSolverData
directly, like new_data = old_data.filter_sort(filter_spec=...)
(this one will not be modifying in-place). Some things to pay attention to:
ModeSolverData
contains aModeSolverMonitor
which stores the originalmode_spec.filter_spec
. So the sorting method should also update that.- Something that can be slightly confusing, but I think it's ok, is that the user can use this method to explore various sortings locally, but then they can't use the sorted data as a mode source or monitor directly. They need to pass the desired filter spec to the mode spec of their object.
- For
ModeSimulationData
, they would have to do something likenew_modes = mode_sim_data.modes.filter_sort(...)
, i.e. we don't create newModeSimulationData
, just a newModeSolverData
.
Does this plan sound good? Any other caveats that come to mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, this reminds me, what if the user wants to filter on multiple properties? Should we have an ordered list of filter_specs?
then if you already had a filter spec, and you filter again, it can concatenate to this list. Then the entire list can be used in a mode source.
otherwise sounds good. Although there’s no reason not to also allow calling this directly on ModeSimulationData I suppose
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I guess I could add it to ModeSimulationData - I just need to update the associated simulation
too...
List of filtering sounds... too much honestly :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, but it’s needed if you want every use case to be translatable into a modeSource
_ = td.ModeSpec(num_modes=1, filter_pol="te", filter_spec=td.ModeFilterSpec()) | ||
|
||
|
||
def test_modes_filter_sort(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this test the case where we filter only and don't sort? maybe need to check test coverage again after filtering was added?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll expand the testing, but generally test coverage does not check whether different pathways are tested together vs separately. It just tests whether every line is called by some test. So e.g. a test that does filtering only and a test that does sorting only will already pass test coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think line 1424 of mode_solver.py was not tested
The issue that @yuanshen-flexcompute thought he was seeing was related to the fact that by default, we have |
I see, that’s a good point. So this new sorting is frequency independent, while track_freq couples different frequencies. Currently, track_freq is applied second. Since they are both sorting, it could be nice to unify their api in mode_spec. Or could just validate that only one or the other is used. |
Addresses the new suggestion in #1391 to add more advanced ways to sort modes in the mode solver (and mode sources and monitors).
Probably we should still keep the issue open as I should also implement the FilterSpec as discussed in that issue.
@yuanshen-flexcompute does the current PR help with some of your use cases that made you bring it up though?
Greptile Overview
Updated On: 2025-10-07 08:34:20 UTC
Summary
This PR adds advanced mode sorting functionality to the Tidy3D mode solver by introducing a new `ModeSortSpec` class and integrating it into the `ModeSpec` configuration. The implementation addresses user feedback from issue #1391 where users were spending significant time manually filtering and sorting modes after simulation.The core addition is the
ModeSortSpec
class which allows users to sort modes by various physical properties including effective index (n_eff
), loss coefficient (k_eff
), TE polarization fraction (TE_fraction
), waveguide TE/TM fractions (wg_TE_fraction
,wg_TM_fraction
), and mode area (mode_area
). Users can specify sorting direction (ascending/descending) and optionally provide a reference value to sort by proximity to that value.The implementation integrates seamlessly into the existing mode solver pipeline through a new
_sort_modes()
method that operates onModeSolverData
after polarization filtering but before frequency tracking. The sorting is applied per-frequency to handle cases where mode properties vary across the frequency spectrum. A mutual exclusivity validator ensures that the newsort_spec
cannot be used simultaneously with the legacyfilter_pol
field to prevent conflicting behaviors.To support the sorting functionality, three new property aliases were added to the
ModeData
class:TE_fraction
,wg_TE_fraction
, andwg_TM_fraction
. These provide more convenient access to polarization fraction data and are used by themodes_info
method for consistency. TheModeSortSpec
class is properly exported through the main__init__.py
file to make it available in the public API.Important Files Changed
Changed Files
ModeSortSpec
class andsort_spec
field toModeSpec
with mutual exclusivity validation_sort_modes()
method and helper functions for mode reorderingModeSortSpec
class to public APIConfidence score: 4/5
Sequence Diagram