Skip to content

Conversation

@momchil-flex
Copy link
Collaborator

@momchil-flex momchil-flex commented Dec 4, 2025

  • Introduces drop_modes boolean, if True, just drop modes that don't make the filtering threshold. The caveat here is that the number of modes might depend on the frequency. The choice that was made was to do the dropping after the tracking, and then just do the intersection and keep the smallest number of modes that meet the filtering criterion at every frequency.
  • Introduces fill_fraction_box as a filtering key which then requires a bounding_box to be passed to the sort_spec. Also a fill_fraction(box) method to the mode solver data, as well as a property fill_fraction_box that uses the box defined in the stored mode_spec.sort_spec.

Greptile Overview

Greptile Summary

This PR adds functionality to drop modes that don't meet filtering thresholds and introduces a new fill_fraction_box filtering metric for mode solvers.

Major Changes:

  • Added keep_modes parameter to ModeSortSpec which can be set to "filtered" to drop modes not passing the filter, or an integer to keep only the first N modes
  • Introduced fill_fraction_box as a new filtering/sorting key that computes the field-energy fill fraction within a specified bounding box
  • Added bounding_box field to ModeSortSpec required when using fill_fraction_box
  • Implemented fill_fraction() method and fill_fraction_box property on ModeSolverData
  • Added _apply_mode_subset() helper method to extract a subset of modes after filtering
  • Mode dropping happens after tracking to ensure consistent mode selection across frequencies

Issues Found:

  • Critical: Indentation bug in sort_modes at monitor_data.py:2622-2638 will cause UnboundLocalError when keep_modes == "all" (the default case)
  • Duplicate validation logic that will never execute
  • Hardcoded tolerance value instead of using named constant
  • Minor syntax error in validation message

Confidence Score: 1/5

  • This PR contains a critical indentation bug that will cause runtime errors in the default use case
  • The indentation error at monitor_data.py:2622-2638 is a blocking issue. When keep_modes == "all" (the default), the code attempts to access keep_mask which is undefined outside the if block on line 2606, causing UnboundLocalError. This affects the default behavior and will break existing functionality.
  • tidy3d/components/data/monitor_data.py requires immediate attention for the critical indentation bug in the sort_modes method

Important Files Changed

File Analysis

Filename Score Overview
tidy3d/components/mode_spec.py 4/5 Added keep_modes field and fill_fraction_box filter key with validators. Minor syntax error in validation message.
tidy3d/components/data/monitor_data.py 1/5 Implemented fill_fraction computation and mode dropping logic. Critical indentation bug in sort_modes causing UnboundLocalError with certain keep_modes values. Also contains duplicate validation and hardcoded tolerance.
tests/test_plugins/test_mode_solver.py 4/5 Added comprehensive tests for drop_modes functionality and fill_fraction_box filter. Test coverage is thorough including edge cases.

Sequence Diagram

sequenceDiagram
    participant User
    participant ModeSortSpec
    participant ModeSolverData
    participant sort_modes
    participant fill_fraction_box

    User->>ModeSortSpec: Create with keep_modes="filtered" or int
    ModeSortSpec->>ModeSortSpec: Validate filter_key required
    ModeSortSpec->>ModeSortSpec: Validate bounding_box for fill_fraction_box
    
    User->>ModeSolverData: Call sort_modes(sort_spec)
    ModeSolverData->>sort_modes: Execute sorting logic
    
    alt filter_key == "fill_fraction_box"
        sort_modes->>fill_fraction_box: Compute fill fraction metric
        fill_fraction_box->>fill_fraction_box: Validate bounding box intersection
        fill_fraction_box->>fill_fraction_box: Create mask within bounding box
        fill_fraction_box->>fill_fraction_box: Calculate weighted intensity ratio
        fill_fraction_box-->>sort_modes: Return fill fraction values
    else other filter_key
        sort_modes->>ModeSolverData: Get metric (n_eff, k_eff, etc)
    end
    
    sort_modes->>sort_modes: Filter modes by threshold (per frequency)
    sort_modes->>sort_modes: Sort modes within groups
    sort_modes->>sort_modes: Track modes across frequencies (if enabled)
    
    alt keep_modes == "filtered"
        sort_modes->>sort_modes: Re-evaluate filter after tracking
        sort_modes->>sort_modes: Keep only modes passing filter at ALL freqs
        sort_modes->>sort_modes: Call _apply_mode_subset
        sort_modes->>sort_modes: Update num_modes to filtered count
    else keep_modes == int
        sort_modes->>sort_modes: Re-evaluate filter after tracking
        sort_modes->>sort_modes: Keep first keep_modes modes
        sort_modes->>sort_modes: Call _apply_mode_subset
        sort_modes->>sort_modes: Update num_modes to keep_modes
    end
    
    sort_modes-->>ModeSolverData: Return sorted/filtered data
    ModeSolverData-->>User: Return new ModeSolverData with updated modes
Loading

@momchil-flex momchil-flex force-pushed the momchil/drop_filtered_modes branch from 87f4819 to 636f5c9 Compare December 4, 2025 14:34
@caseyflex
Copy link
Contributor

@momchil-flex thanks for this!

I think in terms of API, I would prefer a field keep_num_modes or something, so that after the filtering and sorting is applied we can just keep the top keep_num_modes. Instead of drop_modes. The default can be None to keep all modes.

If you want I can take over this PR and make the change

@momchil-flex
Copy link
Collaborator Author

momchil-flex commented Dec 8, 2025

I like the suggestion in that the mode spec would still always produce a deterministic number of modes.

But in terms of functionality, doesn't it sound not adequate e.g. for the EME filtering? Like if you strictly want to drop modes that don't meet a given criterion, this is not going to achieve that, or would achieve in some cases by chance?

Maybe you're imagining having a separate filtering done in EME, which I think could make sense.

@caseyflex
Copy link
Contributor

I like the suggestion in that the mode spec would still always produce a deterministic number of modes.

But in terms of functionality, doesn't it sound not adequate e.g. for the EME filtering? Like if you strictly want to drop modes that don't meet a given criterion, this is not going to achieve that, or would achieve in some cases by chance?

Maybe you're imagining having a separate filtering done in EME, which I think could make sense.

It’s the closest you can easily get to deterministic number of modes, although not perfect. The sim coeffs can tell you how many modes were actually kept; if it’s fewer than the number requested, then you may need to increase num_modes for the computation, or loosen the filter

@momchil-flex
Copy link
Collaborator Author

Ok yea go for it.

@caseyflex
Copy link
Contributor

Ok yea go for it.

Maybe a field “keep_modes” which can be “all”, “filtered”, or an integer

@caseyflex caseyflex force-pushed the momchil/drop_filtered_modes branch 2 times, most recently from 7bd98b6 to 86fea1d Compare December 12, 2025 23:16
@caseyflex caseyflex force-pushed the momchil/drop_filtered_modes branch from 86fea1d to f618f6b Compare December 12, 2025 23:26
Copy link
Contributor

@caseyflex caseyflex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@momchil-flex I kept support for your method via keep_modes="filtered".

I also added the option to have keep_modes be an integer, in which case it just keeps that many modes regardless of filtering, warning however if it keeps some modes that didn't pass the filer.

@caseyflex caseyflex marked this pull request as ready for review December 12, 2025 23:27
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +611 to +614
raise ValidationError(
"ModeSortSpec.keep_modes cannot be larger than 'num_modes' ."
f"Currently these are {val.keep_modes} and {num_modes}."
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The error message could be more descriptive. Consider explaining the relationship between these parameters more clearly for better user experience.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tidy3d/components/mode_spec.py
Line: 611:614

Comment:
**style:** The error message could be more descriptive. Consider explaining the relationship between these parameters more clearly for better user experience.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 13, 2025

Diff Coverage

Diff: origin/develop...HEAD, staged and unstaged changes

  • tidy3d/components/data/monitor_data.py (92.3%): Missing lines 753,764,811,2431,2437,2446,2463,2522,2543,2590,2618
  • tidy3d/components/mode_spec.py (100%)

Summary

  • Total: 166 lines
  • Missing: 11 lines
  • Coverage: 93%

tidy3d/components/data/monitor_data.py

  749             upper_bound = upper[axis_idx]
  750             masks_1d.append((coord_vals >= lower_bound) & (coord_vals <= upper_bound))
  751 
  752         if len(masks_1d) != 2:
! 753             raise DataError("Bounding box masking currently supports planar monitors only.")
  754 
  755         mask_values = (masks_1d[0][:, None] & masks_1d[1][None, :]).astype(float)
  756         mask = DataArray(mask_values, coords={dim: coords[dim] for dim in tan_dims}, dims=tan_dims)
  757         return mask

  760         """Ensure bounding box intersects the monitor plane."""
  761 
  762         zero_dims = self.monitor.zero_dims
  763         if len(zero_dims) != 1:
! 764             raise DataError("Bounding box fill fraction requires a planar monitor.")
  765 
  766         normal_axis = zero_dims[0]
  767         plane_coord = self.monitor.center[normal_axis]
  768         lower, upper = bounding_box.bounds

  807 
  808         sort_spec = getattr(self.monitor.mode_spec, "sort_spec", None)
  809         bounding_box = None if sort_spec is None else sort_spec.bounding_box
  810         if bounding_box is None:
! 811             raise DataError(
  812                 "ModeSortSpec.bounding_box must be set to access 'fill_fraction_box' metric."
  813             )
  814         return self.fill_fraction(bounding_box)

  2427         """
  2428 
  2429         subset_inds_2d = np.asarray(subset_inds_2d, dtype=int)
  2430         if subset_inds_2d.ndim != 2:
! 2431             raise DataError(
  2432                 "subset_inds_2d must be a 2D array of shape (num_freqs, num_modes_keep)."
  2433             )
  2434 
  2435         num_freqs, num_keep = subset_inds_2d.shape

  2433             )
  2434 
  2435         num_freqs, num_keep = subset_inds_2d.shape
  2436         if num_keep == 0:
! 2437             raise DataError("Cannot create a mode subset with zero modes.")
  2438 
  2439         num_modes_full = self.n_eff["mode_index"].size
  2440 
  2441         modify_data = {}

  2442         new_mode_index_coord = np.arange(num_keep)
  2443 
  2444         for key, data in self.data_arrs.items():
  2445             if "mode_index" not in data.dims or "f" not in data.dims:
! 2446                 continue
  2447 
  2448             dims_orig = tuple(data.dims)
  2449             coords_out = {
  2450                 k: (v.values if hasattr(v, "values") else np.asarray(v))

  2459 
  2460             arr = np.moveaxis(data.data, src_order, range(data.ndim))
  2461             nf, nm = arr.shape[0], arr.shape[-1]
  2462             if nf != num_freqs or nm != num_modes_full:
! 2463                 raise DataError(
  2464                     "subset_inds_2d shape does not match array shape in _apply_mode_subset."
  2465                 )
  2466 
  2467             arr2 = arr.reshape(nf, -1, nm)

  2518         num_modes = data.n_eff["mode_index"].size
  2519         all_inds = np.arange(num_modes)
  2520         keep_modes = getattr(sort_spec, "keep_modes", False)
  2521         if keep_modes == "filtered" and sort_spec.filter_key is None:
! 2522             raise ValidationError("ModeSortSpec.keep_modes requires 'filter_key' to be set.")
  2523 
  2524         # Helper to compute ordered indices within a subset
  2525         def _order_indices(indices, vals_all, sort_order):
  2526             if indices.size == 0:

  2539             if key is None:
  2540                 return None
  2541             if key == "fill_fraction_box":
  2542                 if sort_spec is None or sort_spec.bounding_box is None:
! 2543                     raise ValidationError(
  2544                         "ModeSortSpec.bounding_box must be defined when using 'fill_fraction_box'."
  2545                     )
  2546                 if fill_fraction_metric is None:
  2547                     fill_fraction_metric = data.fill_fraction_box

  2586                 data_sorted = data.updated_copy(
  2587                     path="monitor/mode_spec", sort_spec=sort_spec, deep=False, validate=False
  2588                 )
  2589             else:
! 2590                 data_sorted = data
  2591         else:
  2592             data_sorted = data._apply_mode_reorder(sort_inds_2d)
  2593             data_sorted = data_sorted.updated_copy(
  2594                 path="monitor/mode_spec", sort_spec=sort_spec, deep=False, validate=False

  2614                 vals = filter_metric_sorted.isel(f=ifreq).values
  2615                 if sort_spec.filter_order == "over":
  2616                     mask = vals >= sort_spec.filter_reference
  2617                 else:
! 2618                     mask = vals <= sort_spec.filter_reference
  2619                 masks_after.append(mask)
  2620 
  2621             keep_mask = np.all(np.stack(masks_after, axis=0), axis=0)
  2622         if keep_modes == "filtered":

@momchil-flex
Copy link
Collaborator Author

Sorry, accidentally closed this. Sounds good for your change, I guess we'll work on finalizing this after the holidays but good for testing for now!

@momchil-flex momchil-flex reopened this Dec 15, 2025
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

lower, upper = bounding_box.bounds
lower_bound = lower[normal_axis]
upper_bound = upper[normal_axis]
tol = 1e-9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: hardcoded tolerance value 1e-9 should be defined as a named constant

Suggested change
tol = 1e-9
tol = fp_eps

Context Used: Rule from dashboard - Avoid hardcoding values ("magic numbers") that can be programmatically derived from data; use named ... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: tidy3d/components/data/monitor_data.py
Line: 771:771

Comment:
**style:** hardcoded tolerance value `1e-9` should be defined as a named constant

```suggestion
        tol = fp_eps
```

**Context Used:** Rule from `dashboard` - Avoid hardcoding values ("magic numbers") that can be programmatically derived from data; use named ... ([source](https://app.greptile.com/review/custom-context?memory=6661caa7-319d-4caf-8111-723b32818d62))

How can I resolve this? If you propose a fix, please make it concise.

num_modes = values.get("num_modes")
if val.keep_modes > num_modes:
raise ValidationError(
"ModeSortSpec.keep_modes cannot be larger than 'num_modes' ."
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: extra space before period in error message

Suggested change
"ModeSortSpec.keep_modes cannot be larger than 'num_modes' ."
"ModeSortSpec.keep_modes cannot be larger than 'num_modes'."
Prompt To Fix With AI
This is a comment left during a code review.
Path: tidy3d/components/mode_spec.py
Line: 612:612

Comment:
**syntax:** extra space before period in error message

```suggestion
                        "ModeSortSpec.keep_modes cannot be larger than 'num_modes'."
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +2520 to +2522
keep_modes = getattr(sort_spec, "keep_modes", False)
if keep_modes == "filtered" and sort_spec.filter_key is None:
raise ValidationError("ModeSortSpec.keep_modes requires 'filter_key' to be set.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: duplicate validation - this validation is already performed by the pydantic validator _drop_requires_filter in ModeSortSpec at mode_spec.py:112-117, so it will never execute here

Context Used: Rule from dashboard - When modifying a piece of logic, ensure the change is propagated to all independent functions or cal... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: tidy3d/components/data/monitor_data.py
Line: 2520:2522

Comment:
**logic:** duplicate validation - this validation is already performed by the pydantic validator `_drop_requires_filter` in `ModeSortSpec` at mode_spec.py:112-117, so it will never execute here

**Context Used:** Rule from `dashboard` - When modifying a piece of logic, ensure the change is propagated to all independent functions or cal... ([source](https://app.greptile.com/review/custom-context?memory=809457d4-6b33-46ed-b49b-37057d02807e))

How can I resolve this? If you propose a fix, please make it concise.

masks_after.append(mask)

keep_mask = np.all(np.stack(masks_after, axis=0), axis=0)
if keep_modes == "filtered":
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: keep_mask used here but only defined inside the conditional block above. if keep_modes == "all", this line would cause UnboundLocalError

Suggested change
if keep_modes == "filtered":
elif keep_modes == "filtered":
Prompt To Fix With AI
This is a comment left during a code review.
Path: tidy3d/components/data/monitor_data.py
Line: 2622:2622

Comment:
**logic:** `keep_mask` used here but only defined inside the conditional block above. if `keep_modes == "all"`, this line would cause `UnboundLocalError`

```suggestion
        elif keep_modes == "filtered":
```

How can I resolve this? If you propose a fix, please make it concise.

num_modes_sorted = filter_metric_sorted.sizes["mode_index"]
if keep_mask.sum() < num_modes_sorted:
keep_inds = np.where(keep_mask)[0]
elif isinstance(keep_modes, int):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: keep_mask used here but only defined inside the conditional block. this causes UnboundLocalError when keep_modes is an integer but the check on line 2606 is False

Prompt To Fix With AI
This is a comment left during a code review.
Path: tidy3d/components/data/monitor_data.py
Line: 2631:2631

Comment:
**logic:** `keep_mask` used here but only defined inside the conditional block. this causes `UnboundLocalError` when `keep_modes` is an integer but the check on line 2606 is False

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +2622 to +2638
if keep_modes == "filtered":
if not np.any(keep_mask):
raise ValidationError(
"Filtering removes all modes; relax the filter threshold or change 'keep_modes'."
)

num_modes_sorted = filter_metric_sorted.sizes["mode_index"]
if keep_mask.sum() < num_modes_sorted:
keep_inds = np.where(keep_mask)[0]
elif isinstance(keep_modes, int):
if keep_mask.sum() < keep_modes:
log.warning(
f"Filtering with 'keep_modes={keep_modes}' keeps "
f"more than the '{keep_mask.sum()}' modes which pass the filter. Consider "
"relaxing the filter threshold or changing 'keep_modes'."
)
keep_inds = np.arange(keep_modes)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: indentation error causes incorrect control flow. lines 2622-2638 should be indented to be inside the if keep_modes == "filtered" or isinstance(keep_modes, int): block that starts on line 2606, since they reference keep_mask which is only defined within that block. currently, if keep_modes == "all", line 2622 would attempt to access undefined keep_mask

Suggested change
if keep_modes == "filtered":
if not np.any(keep_mask):
raise ValidationError(
"Filtering removes all modes; relax the filter threshold or change 'keep_modes'."
)
num_modes_sorted = filter_metric_sorted.sizes["mode_index"]
if keep_mask.sum() < num_modes_sorted:
keep_inds = np.where(keep_mask)[0]
elif isinstance(keep_modes, int):
if keep_mask.sum() < keep_modes:
log.warning(
f"Filtering with 'keep_modes={keep_modes}' keeps "
f"more than the '{keep_mask.sum()}' modes which pass the filter. Consider "
"relaxing the filter threshold or changing 'keep_modes'."
)
keep_inds = np.arange(keep_modes)
if keep_modes == "filtered":
if not np.any(keep_mask):
raise ValidationError(
"Filtering removes all modes; relax the filter threshold or change 'keep_modes'."
)
num_modes_sorted = filter_metric_sorted.sizes["mode_index"]
if keep_mask.sum() < num_modes_sorted:
keep_inds = np.where(keep_mask)[0]
elif isinstance(keep_modes, int):
if keep_mask.sum() < keep_modes:
log.warning(
f"Filtering with 'keep_modes={keep_modes}' keeps "
f"more than the '{keep_mask.sum()}' modes which pass the filter. Consider "
"relaxing the filter threshold or changing 'keep_modes'."
)
keep_inds = np.arange(keep_modes)
Prompt To Fix With AI
This is a comment left during a code review.
Path: tidy3d/components/data/monitor_data.py
Line: 2622:2638

Comment:
**logic:** indentation error causes incorrect control flow. lines 2622-2638 should be indented to be inside the `if keep_modes == "filtered" or isinstance(keep_modes, int):` block that starts on line 2606, since they reference `keep_mask` which is only defined within that block. currently, if `keep_modes == "all"`, line 2622 would attempt to access undefined `keep_mask`

```suggestion
        if keep_modes == "filtered":
            if not np.any(keep_mask):
                raise ValidationError(
                    "Filtering removes all modes; relax the filter threshold or change 'keep_modes'."
                )

            num_modes_sorted = filter_metric_sorted.sizes["mode_index"]
            if keep_mask.sum() < num_modes_sorted:
                keep_inds = np.where(keep_mask)[0]
        elif isinstance(keep_modes, int):
            if keep_mask.sum() < keep_modes:
                log.warning(
                    f"Filtering with 'keep_modes={keep_modes}' keeps "
                    f"more than the '{keep_mask.sum()}' modes which pass the filter. Consider "
                    "relaxing the filter threshold or changing 'keep_modes'."
                )
            keep_inds = np.arange(keep_modes)
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants