Skip to content

[11/13][Adjoint Module] Subpixel-smooth dispersive materials and differentiate their shape - #3293

Draft
smartalecH wants to merge 3 commits into
fix/adjoint-discrete-dispersionfrom
feat/adjoint-dispersive-smoothing
Draft

[11/13][Adjoint Module] Subpixel-smooth dispersive materials and differentiate their shape#3293
smartalecH wants to merge 3 commits into
fix/adjoint-discrete-dispersionfrom
feat/adjoint-dispersive-smoothing

Conversation

@smartalecH

@smartalecH smartalecH commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #3287.

Second-order subpixel smoothing for dispersive and conductive materials, and the matching shape derivative, which is what a metal reflector's position needs.

  • Routes sigma through a volume-aware hook so a pole's strength can be averaged over a voxel rather than point-sampled.
  • Averages sigma across a geometric interface, linear in fill, excluding the multilevel and gyrotropic cases that have no meaningful average.
  • Extends the analytic dA/du to dispersive objects: previously geometry_addgradient contracted only the real non-dispersive tensor, so a Drude block's poles contributed nothing and the gradient described a structure that was never simulated.

Agrees with a finite difference to 0.017%. The union-of-poles construction is preserved throughout -- each medium's sigma is scaled by its own weight and poles are never blended, which keeps the tensor positive semidefinite and avoids the non-passive [[0,s],[s,0]] form behind #666 / #3229.

`structure_chunk::set_chi1inv` volume-averages the instantaneous permittivity
while `add_susceptibility` point-samples sigma, so a dispersive material
switches on a whole pixel at a time as an object moves.  That is a staircase,
and a staircase has no derivative -- the same wall the geometry gradients hit,
reached through the material rather than the shape.

Add `material_function::eff_sigma_row`, standing to `sigma_row` as
`eff_chi1inv_row` stands to `chi1p1`, and call it from the two sigma loops with
the pixel volume the chi1inv loop already uses.  The default implementation
samples the volume centre, so this is behaviour-preserving on its own: nothing
overrides it yet.

The sigma loops now index in ivec like the chi1inv loop above them, since the
half-pixel offdiagonal shift and the smoothing diameter have to agree between
the two for the eventual averaging to be consistent.

Verified neutral: a Drude MaterialGrid's transmittance is unchanged bit for
bit, and test_adjoint_dispersion, test_material_grid, test_material_dispersion,
test_dispersive_eigenmode, test_faraday_rotation, test_multilevel_atom,
test_geometry_gradient and test_conductivity all pass.
Taper each medium's pole amplitude by the pixel's filling fraction, using the
same front object, shift and fill that eff_chi1inv_matrix uses, so the
permittivity and the susceptibility describe the same interface.

Swept sub-pixel across one pixel, a Drude slab whose eps_infinity matches the
background -- so that the only position dependence there is comes from sigma:

  point sampled   constant to the last digit at 10 of 11 offsets, then a
                  single 20% jump
  averaged        smooth, symmetric about the pixel centre, and 60x less
                  total variation across the sweep

Linear in fill rather than Kottke, deliberately.  Kottke's normal component is
a harmonic average of a permittivity; there is no analogous exact result for a
resonant amplitude, and a harmonic average against a medium that lacks the pole
would be zero in every boundary pixel.  Scaling each medium's whole sigma
tensor by a non-negative weight and adding keeps the result positive
semidefinite, which is what passivity depends on, and makes the amplitude
exactly linear in fill, so the shape derivative is a constant rather than
something to be differenced.

Multilevel atoms and gyrotropic media are excluded and stay point sampled.  A
multilevel atom carries per-pixel level populations with their own nonlinear
dynamics, so a partially filled pixel is not a weaker gain medium -- without
this exclusion test_multilevel_atom fails.  A gyrotropic sigma is
antisymmetric, so the positive-semidefiniteness argument does not hold for it
either.  Material grids and user materials also stay point sampled: their
material already varies continuously in space, and mixing a level set with a
geometric filling fraction is a separate question.

The pole lookup is factored out of sigma_row into material_sigma_row so both
paths ask the same question of a material.
`geometry_addgradient` contracted only the real, non-dispersive
`eff_chi1inv_matrix`, so a Drude or Lorentz object took a branch that dropped
its poles entirely: the gradient described a structure the forward solve never
ran.  That is the piece a metal reflector's position needs.

Add `eff_chi1inv_row_disp_fill`, the dispersive counterpart of the
`fill_override` on `eff_chi1inv_matrix`.  `eff_chi1inv_row_disp` cannot serve,
because it resolves the material at a point and so knows nothing about a
filling fraction.  The construction matches what the forward solve now runs:
the instantaneous part is Kottke-smoothed, and each pole's amplitude is taken
linearly in fill from whichever medium carries it, exactly as `eff_sigma_row`
writes into the sigma arrays.  Since chi1 is linear in sigma, a pole
contributes `lineshape(freq) * weight * sigma` and the two media's pole lists
never have to be matched against each other -- the same reason
`epsilon_material_grid` can concatenate rather than blend.

The branch is chosen from the materials at *this* interface rather than from
`md->trivial` of the front object.  That distinction matters: `trivial` is a
property of a material, but under mixing it becomes a property of a pixel, so a
plain dielectric sitting in front of a metal would otherwise take the
non-dispersive branch and drop the whole contribution at precisely the boundary
pixels that carry the derivative.

`interface_fill` now hands back both materials, since the caller needs them to
build the mixture.

Against a central difference of the objective, at resolution 20:

  dielectric (control)   0.139%
  Drude metal            0.017%

both inside the 2% the existing shape-derivative tests use, which is set by the
O(dx) accuracy of a shape derivative rather than by anything dispersive.
@smartalecH
smartalecH force-pushed the feat/adjoint-dispersive-smoothing branch from 01b9ea1 to 373fc60 Compare September 4, 2026 17:32
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.

1 participant