Skip to content

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

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

[3/13][Adjoint Module] Fix adjoint source weighting on a symmetry plane#3296
smartalecH wants to merge 2 commits into
fix/source-boundary-depositionfrom
test/adjoint-mirror-symmetry

Conversation

@smartalecH

Copy link
Copy Markdown
Collaborator

Recreated so the whole series can form a single GitHub stack. Native stacked pull requests cannot include PRs opened from a fork, and this branch was previously hosted on one. The content and description are unchanged; earlier discussion is on the superseded PR linked below.

Supersedes #3292.

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.

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 changed the title [3/n][Adjoint Module] Fix adjoint source weighting on a symmetry plane [3/13][Adjoint Module] Fix adjoint source weighting on a symmetry plane Sep 2, 2026
@stevengj

stevengj commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

This makes sense to me.

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)

2 participants