Skip to content

Conversation

@dmarek-flex
Copy link
Contributor

@dmarek-flex dmarek-flex commented Nov 25, 2025

Greptile Overview

Greptile Summary

This PR fixes a bug where the mode solver grid would extend beyond simulation domain boundaries, causing incorrect PEC boundary condition application. The fix implements grid truncation at simulation boundaries and zero-pads fields outside the domain.

Key changes:

  • Added _sim_boundary_positions method to detect PEC boundaries and determine where to truncate the computational grid
  • Implemented truncation logic in compute_modes that clips coordinates, permittivity, permeability, and basis fields to simulation bounds
  • Added zero-padding to restore fields to original grid size after solving on truncated domain
  • Added warnings for unsupported boundary types (PMC, Periodic, Bloch) when mode plane intersects them
  • Added comprehensive tests verifying zero fields outside simulation and PEC boundary conditions

The implementation correctly handles the core issue, but there's a minor inconsistency in the boundary intersection detection logic that could cause edge cases.

Confidence Score: 4/5

  • This PR is generally safe to merge with one logical inconsistency that should be addressed
  • The implementation is well-designed with comprehensive tests, proper documentation, and correct changelog entry. However, there's a logical inconsistency in the boundary intersection detection (using np.isclose vs fp_eps threshold) that could cause edge case failures. The core truncation and zero-padding logic appears sound
  • Pay close attention to tidy3d/components/mode/mode_solver.py for the boundary intersection logic inconsistency

Important Files Changed

File Analysis

Filename Score Overview
CHANGELOG.md 5/5 Added changelog entry describing the bug fix for mode solver grid extending beyond simulation bounds
tests/test_plugins/test_mode_solver.py 4/5 Added comprehensive tests for PEC boundary truncation and zero-padding, including warning tests for unsupported boundary types. Tests verify field values outside simulation bounds and at PEC boundaries
tidy3d/components/mode/mode_solver.py 4/5 Added _sim_boundary_positions method to detect PEC boundaries and determine truncation positions. Passes boundary positions to solver for grid truncation. Minor inconsistency in boundary checking logic
tidy3d/components/mode/solver.py 4/5 Implements core truncation logic: truncates grid/eps/mu/fields to simulation bounds when needed, then zero-pads results. Adds _truncate_medium_data helper method

Sequence Diagram

sequenceDiagram
    participant MS as ModeSolver
    participant MSC as ModeSolver._solve_single_freq
    participant SBP as ModeSolver._sim_boundary_positions
    participant CM as compute_modes (solver.py)
    participant TMD as _truncate_medium_data
    
    MS->>MS: User calls .data property
    MS->>MSC: _solve_single_freq(freq, coords, symmetry)
    MSC->>SBP: Get PEC boundary positions
    SBP->>SBP: Check boundary types (PEC, PMC, etc.)
    SBP->>SBP: Determine if solver grid intersects boundaries
    SBP-->>MSC: Return (pos_min, pos_max) tuples
    
    MSC->>CM: compute_modes(eps_cross, coords, ..., sim_pec_pos_min, sim_pec_pos_max)
    
    CM->>CM: Calculate truncation indices from PEC positions
    CM->>CM: Check if truncation needed (trim_min != [0,0] or trim_max != [Nx,Ny])
    
    alt Truncation needed
        CM->>CM: Truncate coords arrays
        CM->>TMD: Truncate eps_cross
        TMD-->>CM: Truncated eps
        CM->>TMD: Truncate mu_cross (if present)
        TMD-->>CM: Truncated mu
        CM->>CM: Truncate split_curl_scaling (if present)
        CM->>CM: Truncate solver_basis_fields (if present)
    end
    
    CM->>CM: Solve eigenvalue problem on truncated grid
    CM->>CM: Reshape fields to (2, 3, Nx_trunc, Ny_trunc, 1, num_modes)
    
    alt Truncation was applied
        CM->>CM: Zero-pad fields back to original size
        Note over CM: np.pad with trim_min/trim_max offsets
    end
    
    CM-->>MSC: Return (n_complex, fields, eps_spec)
    MSC-->>MS: Return mode data with correct boundary conditions
Loading

@dmarek-flex dmarek-flex self-assigned this Nov 25, 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, 1 comment

Edit Code Review Agent Settings | Greptile

@github-actions
Copy link
Contributor

github-actions bot commented Nov 25, 2025

Diff Coverage

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

  • tidy3d/components/mode/mode_solver.py (97.5%): Missing lines 1680
  • tidy3d/components/mode/solver.py (86.8%): Missing lines 166,170,1136,1138,1140

Summary

  • Total: 78 lines
  • Missing: 6 lines
  • Coverage: 92%

tidy3d/components/mode/mode_solver.py

Lines 1676-1684

  1676     ) -> tuple[list[float], list[dict[str, ArrayComplex4D]], list[EpsSpecType]]:
  1677         """Call the mode solver at all requested frequencies."""
  1678         if tidy3d_extras["use_local_subpixel"]:
  1679             subpixel_ms = tidy3d_extras["mod"].SubpixelModeSolver.from_mode_solver(self)
! 1680             return subpixel_ms._solve_all_freqs(
  1681                 coords=coords,
  1682                 symmetry=symmetry,
  1683             )

tidy3d/components/mode/solver.py

Lines 162-174

  162             eps_cross = cls._truncate_medium_data(eps_cross, cell_min, cell_max)
  163 
  164             # Truncate mu_cross if provided
  165             if mu_cross is not None:
! 166                 mu_cross = cls._truncate_medium_data(mu_cross, cell_min, cell_max)
  167 
  168             # Truncate split_curl_scaling if provided
  169             if split_curl_scaling is not None:
! 170                 split_curl_scaling = tuple(
  171                     s[cell_min[0] : cell_max[0], cell_min[1] : cell_max[1]]
  172                     for s in split_curl_scaling
  173                 )

Lines 1132-1144

  1132 
  1133         if isinstance(mat_data, Numpy):
  1134             # Tensorial format: shape (9, Nx, Ny)
  1135             return mat_data[:, x_slice, y_slice]
! 1136         if len(mat_data) == 9:
  1137             # Nine separate 2D arrays
! 1138             return tuple(arr[x_slice, y_slice] for arr in mat_data)
  1139 
! 1140         raise ValueError("Wrong input to mode solver permittivity/permeability truncation!")
  1141 
  1142     @staticmethod
  1143     def format_medium_data(mat_data):
  1144         """

@dmarek-flex dmarek-flex force-pushed the dmarek/FXC-4112-PEC-boundary-position-not-respected-by-ModeSolver branch from f179f91 to 6bb0d8f Compare November 26, 2025 16:58
@dmarek-flex
Copy link
Contributor Author

@greptileai another round

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.

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

… PEC boundary conditions from the Simulation
@dmarek-flex dmarek-flex force-pushed the dmarek/FXC-4112-PEC-boundary-position-not-respected-by-ModeSolver branch from d1ada2b to 3df4c17 Compare November 26, 2025 17:38
f"Fields will not wrap around periodically."
)
# ABC boundaries: no truncation, no warning (fields can extend)
return (tuple(pos_min), tuple(pos_max))
Copy link
Collaborator

Choose a reason for hiding this comment

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

So this fix seems to be specific to the case where the enclosing Simulation has e.g. PEC boundaries.

I am wondering if this still excludes problematic cases where the enclosing Simulation is large, but the mode plane itself does not impose the PEC boundaries on the analytical plane boundaries, but instead on whatever the discretized version of the plane turns out to be. You guys might have dealt with something like that already, not sure.

If memory serves me right, at least in the new ModeSimulation class we've made a small step towards handling this correctly in the following sense:

  1. If the ModeSimulation is defined with a 3D center, size, as well as a plane argument, then the inaccuracy is technically there. The plane will be discretized based on the larger 3D "simulation" grid, and the discretization and hence where boundaries are applied may not match the analytical plane definition.
  2. If however the ModeSimulation is defined with a 2D size and no extra plane argument, then the grid matches the plane, and the boundaries match the expected location.

So I guess what I'm wondering is if your approach to pass the sim_pec_pos_max, etc. should also be applied in situations like case 1. above. Although I'm not sure what should happen if the analytical plane position just lies somewhere in between the mode plane grid points.. Here your fix works because the boundary location does match a grid point, there just might be an extra pixel, right?

Copy link
Contributor Author

@dmarek-flex dmarek-flex Nov 27, 2025

Choose a reason for hiding this comment

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

I am wondering if this still excludes problematic cases where the enclosing Simulation is large, but the mode plane itself does not impose the PEC boundaries on the analytical plane boundaries, but instead on whatever the discretized version of the plane turns out to be. You guys might have dealt with something like that already, not sure.

Yes, so far I was trying to match existing behavior, but fix the bug. So if the mode plane is smaller than the Simulation plane bounds, there is no change in behavior when compared to develop. It is not clear to me what to do if the mode plane boundaries don't line up with grid boundaries. I think changing the grid is never a good idea, especially since these modes might be injected into an FDTD simulation. And if we choose the closest grid boundary, or say the closest grid boundary that encompasses the mode plane, then basically we have the same problem (adding additional space), just potentially not quite as bad as the one complete cell in the bug.

Here your fix works because the boundary location does match a grid point, there just might be an extra pixel, right?

Correct, sim_pec_pos_max might as well be an index into the grid boundaries. I only want to support the PEC boundary matching an existing grid boundary, because otherwise the mode solver grid would have to change slightly to respect a new location of the PEC boundary.

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