Skip to content

PEC gradient support for Box and PolySlab geometries #2724

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

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

groberts-flex
Copy link
Contributor

@groberts-flex groberts-flex commented Aug 6, 2025

This PR adds functionality for computing derivatives for Box and PolySlab geometries when the material in them is composed of PEC. This requires (1) using both electric and magnetic fields when computing the boundary gradients meaning that in cases of PEC, we add additional field components to the adjoint monitors and use them accordingly and (2) that we are careful about interpolation around the PEC boundaries to make sure not to interpolate with the fields inside of the material which are 0. There is also special handling for 2D structures that have edge singularities and numerical tests for evaluating the gradient accuracy. The result of running these unit tests is here: https://docs.google.com/presentation/d/1ddA1V_efdXi1CZ5Soal72crCDk856zUX3FFba6oa-zo/edit?usp=sharing

The gradient performance has also been tested in several antenna optimizations including ones where these geometries overlap each other.

Greptile Summary

This PR implements PEC (Perfect Electric Conductor) gradient support for Box and PolySlab geometries in the autograd system. The changes enable shape optimization of metallic structures like antennas by adding the ability to compute derivatives when these geometries contain PEC materials.

The implementation addresses fundamental electromagnetic theory requirements for PEC boundaries. Unlike dielectric materials that only need electric field components for gradient calculations, PEC materials require both electric and magnetic field components due to their unique boundary conditions where tangential electric fields are zero at surfaces. This necessitated significant changes across the autograd pipeline:

  1. Enhanced field monitoring: The adjoint monitors now conditionally include magnetic field components (Hx, Hy, Hz) alongside electric fields when dealing with PEC structures.

  2. Specialized interpolation: PEC regions have zero internal fields, so the implementation uses nearest-neighbor interpolation instead of linear interpolation to avoid sampling zero-valued fields inside PEC boundaries.

  3. Dual gradient formulation: The derivative computation now has separate code paths for PEC and dielectric materials, with PEC calculations incorporating both electric and magnetic field contributions weighted by appropriate electromagnetic constants (MU_0/EPSILON_0).

  4. Coordinate adjustment and singularity handling: Special logic handles 2D PEC structures that exhibit edge singularities, including coordinate snapping to ensure proper field sampling near boundaries.

  5. Comprehensive testing framework: Two extensive test files validate the implementation through finite difference comparisons across various mesh refinements, wavelengths, and geometry configurations.

The changes maintain backward compatibility while extending the autograd capabilities to support PEC geometries, enabling antenna optimization and other applications involving metallic structures. The implementation correctly handles the theoretical distinction between dielectric and PEC boundary conditions in electromagnetic gradient calculations.

Important Files Changed

Click to expand file changes
Filename Score Overview
tidy3d/components/geometry/base.py 4/5 Implements core PEC gradient computation with dual E/H field handling and singularity correction
tidy3d/components/autograd/derivative_utils.py 4/5 Adds PEC-specific interpolation, coordinate adjustment, and magnetic field derivative structures
tidy3d/web/api/autograd/autograd.py 4/5 Extends autograd web API with H-field support and PEC-specific field processing
tidy3d/components/structure.py 4/5 Conditionally adds magnetic field components to adjoint monitors for PEC structures
tidy3d/web/api/autograd/utils.py 4/5 Adds H-field derivative map creation and parameterized field multiplication utilities
tidy3d/components/geometry/polyslab.py 4/5 Updates PolySlab derivative computation with enhanced bounds handling and performance optimizations
tests/test_components/test_autograd_rf_box.py 4/5 Comprehensive test suite validating PEC gradient accuracy through finite difference comparison
tests/test_components/test_autograd_rf_polyslab.py 3/5 Additional validation tests for PolySlab PEC gradients with some code quality issues

Confidence score: 4/5

  • This PR implements a theoretically sound solution for PEC gradient computation with comprehensive testing and maintains backward compatibility
  • Score reflects the complexity of electromagnetic boundary condition handling and potential edge cases in the mathematical formulation
  • Pay close attention to the test files which contain several unused variables and unreachable code sections that should be cleaned up

Sequence Diagram

sequenceDiagram
    participant User
    participant WebAPI as "tidy3d.web.run"
    participant AutogradRun as "_run_primitive"
    participant ForwardSim as "Forward Simulation"
    participant AdjointSim as "Adjoint Simulation"
    participant DerivativeInfo as "DerivativeInfo"
    participant Structure as "Structure"
    participant Geometry as "Box/PolySlab"
    
    User->>WebAPI: "run(simulation, task_name)"
    WebAPI->>WebAPI: "is_valid_for_autograd(simulation)"
    WebAPI->>AutogradRun: "_run(simulation, ...)"
    
    AutogradRun->>AutogradRun: "setup_run(simulation)"
    Note over AutogradRun: Extract traced fields from simulation
    
    AutogradRun->>AutogradRun: "_run_primitive(traced_fields, ...)"
    AutogradRun->>AutogradRun: "setup_fwd(sim_fields, sim_original)"
    
    AutogradRun->>ForwardSim: "run forward simulation with adjoint monitors"
    Note over ForwardSim: Includes E and H field monitors for PEC
    ForwardSim-->>AutogradRun: "sim_data_fwd with field data"
    
    Note over User: When gradient computation is triggered
    AutogradRun->>AutogradRun: "_run_bwd(data_fields_vjp, ...)"
    AutogradRun->>AutogradRun: "setup_adj(data_fields_vjp, ...)"
    
    AutogradRun->>AdjointSim: "run adjoint simulation"
    AdjointSim-->>AutogradRun: "sim_data_adj"
    
    AutogradRun->>AutogradRun: "postprocess_adj(sim_data_adj, ...)"
    
    AutogradRun->>DerivativeInfo: "create DerivativeInfo with E, D, H fields"
    Note over DerivativeInfo: For PEC: includes H_fwd, H_adj fields<br/>and sets is_medium_pec=True
    
    AutogradRun->>Structure: "_compute_derivatives(derivative_info)"
    Structure->>Geometry: "_compute_derivatives(derivative_info)"
    
    alt Box geometry
        Geometry->>Geometry: "_derivative_faces(derivative_info)"
        loop For each face
            Geometry->>Geometry: "_derivative_face(min_max_index, axis_normal)"
            alt PEC material
                Geometry->>Geometry: "integrate E normal field with singularity correction"
                Geometry->>Geometry: "integrate H tangential fields"
                Note over Geometry: Apply MU_0/EPSILON_0 weighting for H fields
            else Dielectric material
                Geometry->>Geometry: "integrate D normal and E tangential fields"
            end
        end
    else PolySlab geometry
        Geometry->>Geometry: "_compute_derivative_vertices(derivative_info)"
        Geometry->>Geometry: "edge_basis_vectors(edges)"
        loop For each edge segment
            Geometry->>DerivativeInfo: "evaluate_gradient_at_points(coords, normals, perps1, perps2)"
            alt PEC material
                DerivativeInfo->>DerivativeInfo: "_adjust_spatial_coords_pec(grid_centers)"
                Note over DerivativeInfo: Snap coordinates outside PEC surface<br/>for nearest interpolation
                DerivativeInfo->>DerivativeInfo: "compute E normal + H tangential contributions"
                DerivativeInfo->>DerivativeInfo: "apply singularity correction for 2D PEC"
            else Dielectric material
                DerivativeInfo->>DerivativeInfo: "compute D normal + E tangential contributions"
            end
        end
    end
    
    Geometry-->>Structure: "gradient values"
    Structure-->>AutogradRun: "vjp_traced_fields"
    AutogradRun-->>User: "gradients w.r.t. traced fields"
Loading

Context used:

Rule - Remove temporary debugging code (print() calls), commented-out code, and other workarounds before finalizing a pull request. (link)
Rule - Use an underscore (_) for loop variables that are intentionally unused. (link)
Rule - Remove commented-out or obsolete code; rely on version control for history. (link)

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.

8 files reviewed, 17 comments

Edit Code Review Bot Settings | Greptile

Comment on lines +1516 to +1518
"Computing slab face derivatives for flat structures is not fully supported and "
"may give zero for the derivative. Try using a structure with a small, but nonzero"
"thickness for slab bound derivatives."
Copy link

Choose a reason for hiding this comment

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

syntax: Missing space in warning message before 'thickness'

Suggested change
"Computing slab face derivatives for flat structures is not fully supported and "
"may give zero for the derivative. Try using a structure with a small, but nonzero"
"thickness for slab bound derivatives."
"Computing slab face derivatives for flat structures is not fully supported and "
"may give zero for the derivative. Try using a structure with a small, but nonzero "
"thickness for slab bound derivatives."

fd_grad = np.squeeze(all_fd_grad_parameters[argmin_convergence_test])
else:
fd_grad = np.squeeze(all_fd_grad_parameters)
valid_mask = np.ones(fd_grad, dtype=bool)
Copy link

Choose a reason for hiding this comment

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

syntax: np.ones() expects shape as first argument, not the array itself

Suggested change
valid_mask = np.ones(fd_grad, dtype=bool)
valid_mask = np.ones(len(fd_grad), dtype=bool)

Comment on lines +435 to +436
dim_um = 1.5 * mesh_wvl_um
dim_um = 1.5 * mesh_wvl_um
Copy link

Choose a reason for hiding this comment

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

style: Variable 'dim_um' is assigned the same value twice

Suggested change
dim_um = 1.5 * mesh_wvl_um
dim_um = 1.5 * mesh_wvl_um
dim_um = 1.5 * mesh_wvl_um

Context Used: Rule - Extract repeated or complex expressions into well-named local variables to improve readability. (link)

Comment on lines +602 to +603
dim_um = 1.5 * mesh_wvl_um
dim_um = 1.5 * mesh_wvl_um
Copy link

Choose a reason for hiding this comment

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

style: Variable 'dim_um' is assigned the same value twice

Suggested change
dim_um = 1.5 * mesh_wvl_um
dim_um = 1.5 * mesh_wvl_um
dim_um = 1.5 * mesh_wvl_um

Context Used: Rule - Extract repeated or complex expressions into well-named local variables to improve readability. (link)

Comment on lines +476 to +477
dim_um = 3 * mesh_wvl_um
dim_um = 3 * mesh_wvl_um
Copy link

Choose a reason for hiding this comment

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

style: duplicate variable assignment - same value assigned to dim_um twice

Suggested change
dim_um = 3 * mesh_wvl_um
dim_um = 3 * mesh_wvl_um
dim_um = 3 * mesh_wvl_um

Context Used: Rule - Remove temporary debugging code (print() calls), commented-out code, and other workarounds before finalizing a pull request. (link)

Comment on lines +659 to +660
dim_um = 3 * mesh_wvl_um
dim_um = 3 * mesh_wvl_um
Copy link

Choose a reason for hiding this comment

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

style: duplicate variable assignment - same value assigned to dim_um twice

Suggested change
dim_um = 3 * mesh_wvl_um
dim_um = 3 * mesh_wvl_um
dim_um = 3 * mesh_wvl_um

Context Used: Rule - Remove temporary debugging code (print() calls), commented-out code, and other workarounds before finalizing a pull request. (link)

if make_H_der_map:
der_map_H = derivative_map_H(fld_fwd=fld_fwd, fld_adj=fld_adj)

return {"E" : der_map_E, "D": der_map_D, "H": der_map_H}
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 colon in dictionary - should be "E": der_map_E for consistency

Suggested change
return {"E" : der_map_E, "D": der_map_D, "H": der_map_H}
return {"E": der_map_E, "D": der_map_D, "H": der_map_H}

Comment on lines +61 to +63
def get_field_key(dim: str, fld_data: typing.Union[td.FieldData, td.PermittivityData]) -> str:
"""Get the key corresponding to the scalar field along this dimension."""
return f"E{dim}" if isinstance(fld_data, td.FieldData) else f"eps_{dim}{dim}"
Copy link

Choose a reason for hiding this comment

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

style: This inner function get_field_key is identical to the one defined in multiply_field_data below and is now unused - should be removed

Context Used: Rule - Remove commented-out or obsolete code; rely on version control for history. (link)

Comment on lines +2571 to +2573
"Derivative of PEC material with less than 2 dimensions is unsupported. "
"Specified PEC box is {dimension}-dimesional"
)
Copy link

Choose a reason for hiding this comment

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

syntax: String formatting error: f-string missing variable interpolation

Suggested change
"Derivative of PEC material with less than 2 dimensions is unsupported. "
"Specified PEC box is {dimension}-dimesional"
)
log.error(
"Derivative of PEC material with less than 2 dimensions is unsupported. "
f"Specified PEC box is {dimension}-dimensional"
)

Copy link
Collaborator

@yaugenst-flex yaugenst-flex left a comment

Choose a reason for hiding this comment

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

Thanks @groberts-flex, huge effort! Mostly comments about general organization.

@@ -66,6 +66,13 @@ class DerivativeInfo:
of the forward and adjoint displacement fields. The normal component of this
dataset is used when computing adjoint gradients for shifting boundaries."""

H_der_map: FieldData
Copy link
Collaborator

Choose a reason for hiding this comment

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

All these H-related fields probably should be optional?

Comment on lines +130 to +133
simulation_bounds: Bound
"""Geometry and simulation intersection bounds.
Bounds corresponding to the minimum intersection between the structure
and the simulation it is contained in."""
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, it would be important to clarify the distinction between these two or remove one.

@@ -138,6 +160,11 @@ class DerivativeInfo:
GeometryGroup handling where it is difficult to automatically evaluate
the inside and outside relative permittivity for each geometry."""

is_medium_pec: bool = False
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just a note, no need to change it in the context of this PR: We should consider making this behavior polymorphic, ie dispatching to different branches of the gradient calculation based on type rather than having explicit flags, especially if we introduce more cases where such separate treatment is necessary.

^
| n (normal direction)
|
_.-~'`-._.-~'`-._ (PEC surface)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Pretty 😂

Copy link
Collaborator

Choose a reason for hiding this comment

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

The evaluate_gradient_at_points has become really long, I think it needs some refactoring. It'd be good to extract the closures into class methods (or even module-level functions) and to split the PEC and dielectric paths into separate methods. So something like:

def evaluate_gradient_at_points(...):
  # does some common pre- & post-processing, then calls one of:

def _evaluate_pec_gradient_at_points(...):
  ...

def _evaluate_dielectric_gradient_at_points(...):
  ...

# adjust coordinates by half a grid point outside boundary such that nearest interpolation
# point snaps to outside the boundary
# adjust_spatial_coords = np.squeeze([spatial_coords - normals * 0.5 * coords_dn])
adjust_spatial_coords = np.squeeze([spatial_coords + normals * 0.5 * coords_dn])
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks somewhat unconventional. Is this equivalent to np.squeeze(x, axis=0)?


# adjust coordinates by half a grid point outside boundary such that nearest interpolation
# point snaps to outside the boundary
# adjust_spatial_coords = np.squeeze([spatial_coords - normals * 0.5 * coords_dn])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove?

Comment on lines +489 to +505
E_fwd_at_coords = {
name: interp(E_fwd_coords_adjusted[name]["coords"])
for name, interp in interpolators["E_fwd"].items()
}
E_adj_at_coords = {
name: interp(E_adj_coords_adjusted[name]["coords"])
for name, interp in interpolators["E_adj"].items()
}

H_fwd_at_coords = {
name: interp(H_fwd_coords_adjusted[name]["coords"])
for name, interp in interpolators["H_fwd"].items()
}
H_adj_at_coords = {
name: interp(H_adj_coords_adjusted[name]["coords"])
for name, interp in interpolators["H_adj"].items()
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

There should probably be a helper method for this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar comment here: Box._derivative_face has become really complex with the addition of integrate_face. This function in particular seems to be doing a lot of things:

  1. Coordinate snapping
  2. Singularity correction
  3. Boundary detection
  4. Actual integration

Similar to the comment on evaluate_gradient_at_points, I think this should be split into functions with clearer responsibilities. With the current approach, it's also not really possible to test integrate_face.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants