Skip to content

[3/n][Adjoint Module] Fix adjoint source weighting on a symmetry plane - #3292

Closed
smartalecH wants to merge 2 commits into
NanoComp:fix/source-boundary-depositionfrom
smartalecH:test/adjoint-mirror-symmetry
Closed

smartalecH wants to merge 2 commits into
NanoComp:fix/source-boundary-depositionfrom
smartalecH:test/adjoint-mirror-symmetry

Conversation

@smartalecH

@smartalecH smartalecH commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Stack

Each PR is based on the one above it, so every diff here is just its own delta.

PR
1 #3277 Fix MaterialGrid design-variable attribution (#1984)
2 #3290 Fix source deposition across chunk boundaries
3 #3292 Fix adjoint source weighting on a symmetry plane (#3291) ← you are here
4 #3280 Enable proper JAX support in the adjoint module
5 #3281 Differentiable angular-spectrum propagation
6 #3283 Adjoint gradients w.r.t. source amplitudes
7 #3284 Launch a mode through a stratified stack
8 #3285 Adjoint gradients w.r.t. geometry (centre and size)
9 #3286 Register a material grid's susceptibilities
10 #3287 Evaluate the dispersive adjoint at the discrete lineshape
11 #3293 Subpixel-smooth dispersive materials and their shape derivative
12 #3294 Scalable Pade extrapolation for DFT monitors (#3217)
13 #3295 3D grating coupler example: fiber, grating and mirror

Fixes #3291.

fourier_sourcedata divides each interpolated adjoint source by the multiplicity of the point it sits at, to undo the replication a reduced symmetric domain applies. In the yee_grid = false branch the amplitude is split over the four Yee corners of a voxel, but the divisor was evaluated once at the voxel centre and reused for all four.

Those disagree exactly on a symmetry plane. A point there is its own image, so its multiplicity is 1, while the centre half a pixel away has 2. The corner on the plane was therefore halved when it should not have been, leaving the adjoint source short.

objective vs. the plane before after
on the plane 0.7484 1.0002
one pixel off 0.8725 1.0002
clear of it 1.0002 1.0002
extended, spanning it (res 20 / 30 / 40) 0.9602 / 0.9715 / 0.9819 1.0002 / 0.9975 / 1.0000

(finite difference = 1; no symmetry declared is 1.0002 throughout)

The on-plane number is exactly 3/4 and resolution independent — the source's three Yee rows carry weights 1/4, 1/2, 1/4 and the halved one is the middle row, so exactly a quarter of the source goes missing. An extended monitor merely spanning the plane dilutes that single row over the O(1/dx) rows it covers, which is why it presented as a first-order error rather than a constant one. The last row is the common arrangement for a symmetric device, so most symmetric adjoint runs were carrying a few percent of gradient error.

The fix takes the multiplicity of the site each quarter is actually written to. Off the plane nothing changes, and with no symmetry declared every multiplicity is 1, so this is a no-op for the vast majority of runs.

Tests

Meep had no adjoint test that declared a symmetry at all. The first commit adds one, including the part that looks like a bug and is not: with Mirror(Y) the design gradient puts all of its magnitude in y >= 0, because Meep builds the structure on the reduced grid volume, so weights below the plane are never read and a weight above it drives both halves at once. Finite differences confirm both halves — zero below the plane, and exactly 2.0000x above it.

Those two commits are separated deliberately: the first records the on-plane and plane-crossing failures as expectedFailure, and the second turns them into plain assertions, so the diff shows the fix flipping them.

test_adjoint_solver.py (44 gradient checks) and test_adjoint_cyl.py both pass unchanged.

@smartalecH smartalecH changed the title Add adjoint tests for simulation mirror symmetry Fix adjoint source weighting on a symmetry plane Sep 2, 2026
@smartalecH
smartalecH marked this pull request as ready for review September 2, 2026 21:47
@smartalecH
smartalecH marked this pull request as draft September 2, 2026 21:56
Meep has no adjoint test that declares a symmetry, and the behaviour is easy to
misread as a bug: with Mirror(Y) the design gradient comes back with all of its
magnitude in y >= 0, which looks like a routine that forgot to mirror the
gradient back over the design region.  It is not.  Meep builds the structure on
the reduced grid volume, so weights below the plane are never read, and a weight
above it drives both halves of the structure at once.  Finite differences
confirm both halves of that: the gradient really is zero below the plane, and
really is exactly twice the no-symmetry value above it.

Two cases are genuinely wrong and are recorded as expected failures rather than
skipped, so that a fix flips them:

  - An objective on the symmetry plane gives 3/4 of the correct gradient.
    Exactly 0.7500 at resolution 40 and 0.7484 at 20, so this is a scaling
    error, not discretization.

  - A monitor spanning the plane -- the usual case -- is low by one row's
    worth: 4.0% at resolution 20, 2.9% at 30, 1.8% at 40, i.e. first order in
    the grid spacing.

Everything with the objective clear of the plane matches a finite difference to
the solver's usual accuracy, which places the fault in how the adjoint source is
weighted on the plane rather than in the gradient accumulation.
fourier_sourcedata divides each interpolated adjoint source by the multiplicity
of the point it sits at, to undo the replication a reduced symmetric domain
applies.  In the yee_grid = false branch the amplitude is split over the four
Yee corners of a voxel, but the divisor was evaluated once at the voxel centre
and reused for all four.

Those disagree exactly on a symmetry plane.  A point there is its own image, so
its multiplicity is 1, while the centre half a pixel away has 2.  The corner on
the plane was therefore halved when it should not have been, leaving the adjoint
source short.

For an objective sitting on the plane the source's three Yee rows carry weights
1/4, 1/2, 1/4 and the halved one is the middle row, so exactly 1/4 of the source
goes missing at any resolution -- the gradient came out at 3/4 of the finite
difference (0.7484 at resolution 20, 0.7500 at 40).  For an extended monitor
merely spanning the plane the same single row is diluted over the O(1/dx) rows
covered, which showed up as a first-order error: 4.0% / 2.9% / 1.8% at
resolution 20 / 30 / 40.

Take the multiplicity of the site each quarter is written to.  Off the plane
nothing changes, and without a symmetry every multiplicity is 1, so this is a
no-op for the vast majority of runs.

The two cases recorded as expected failures in the previous commit now hold, so
they become plain assertions.  test_adjoint_solver.py (44 gradient checks) and
test_adjoint_cyl.py are unaffected.
@smartalecH
smartalecH force-pushed the test/adjoint-mirror-symmetry branch from 159707c to 7388d31 Compare September 2, 2026 22:14
@smartalecH
smartalecH changed the base branch from master to fix/source-boundary-deposition September 2, 2026 22:16
@smartalecH smartalecH changed the title Fix adjoint source weighting on a symmetry plane [3/n][Adjoint Module] Fix adjoint source weighting on a symmetry plane Sep 2, 2026
@smartalecH

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #3296, which is the same branch and the same content, opened from this repository rather than from a fork.

The reason is purely mechanical: this series is a stack of 13 pull requests, each based on the one below it so that every diff is only its own delta. GitHub's native stacked pull requests cannot include a PR opened from a fork, and four of the thirteen — including this one — were. Rather than leave the series half-stacked, all four have been reopened with their branches hosted here so the whole chain can be linked.

Nothing about the change itself has been revised, and the description is carried over verbatim. The discussion and reviews on this PR stay readable here; please continue on #3296.

@smartalecH smartalecH closed this Sep 2, 2026
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.

Adjoint gradient is wrong when the objective monitor lies on a symmetry plane (3/4 on-plane; O(dx) crossing)

1 participant