Skip to content

Fix 3D MaterialGrid subpixel smoothing: kernel normalization (#3258) and gradient consistency (#3259) - #3263

Open
jin-castle wants to merge 2 commits into
NanoComp:masterfrom
jin-castle:pr/subpixel-smoothing-3d
Open

Fix 3D MaterialGrid subpixel smoothing: kernel normalization (#3258) and gradient consistency (#3259)#3263
jin-castle wants to merge 2 commits into
NanoComp:masterfrom
jin-castle:pr/subpixel-smoothing-3d

Conversation

@jin-castle

@jin-castle jin-castle commented Aug 9, 2026

Copy link
Copy Markdown

Fixes #3258 and fixes #3259, with regression tests. (Issue text contains the
full derivations; summary below.)

Defect A (#3258) — kernel normalization. get_uproj_w()'s 3D branch
divides by 4 / 3 * pi * rad^3, where 4 / 3 is integer division. The
smoothing kernel therefore integrates to 4/3 instead of 1, scaling the
smoothed tensor everywhere the material grid has a nonzero gradient
(interface-parallel components by 4/3, perpendicular by 3/4) — for an
isotropic MaterialGrid that is essentially the whole design region, not just
interface pixels. One-character fix: 4.0 / 3.0. The 1D/2D branches were
already correct.

Defect B (#3259) — eps_averaging not propagated to the gradient.
structure_chunk::set_chi1inv() zeroes maxeval when averaging is off, but
set_materials_from_geom_epsilon() stored the raw maxeval into the
geom_epsilon used by get_material_gradient(), and the _set_materials
Python wrapper left the header defaults in place when it skipped
set_materials_from_geom_epsilon entirely. The adjoint therefore
finite-differenced a smoothed operator that the forward solve never used.
Measured: directional finite-difference vs adjoint disagreement of a factor
~78 in 3D with do_averaging=True, eps_averaging=False — the reason 3D
adjoint users have had to disable smoothing entirely.

Convenience switch. Simulation(allow_3d_subpixel=False) turns off
do_averaging on every reachable MaterialGrid at structure initialization
in a 3D run, so smoothing can be disabled in one place instead of per-grid.
Default True leaves behavior unchanged.

Tests (python/tests/test_subpixel_3d.py): analytic kernel-normalization
checks in 1D/2D/3D, and 3D directional finite-difference vs adjoint gradient
checks for smoothing on, off, and the mixed case. Before the fix the mixed
case is off by ~7800% and the smoothing-on case silently solves the
mis-normalized epsilon; after it, all three agree.

Please merge #3264 before this one. The gradient checks above measure a
quantity that #3264 fixes independently, and on this branch alone they are
only accurate while the design region sits inside a single chunk. The
fd/adjoint ratio for the smoothing-on case, measured on this branch by
forcing the split with Simulation(num_chunks=...):

chunks 1 2 4
this branch alone 0.99982 0.99845 0.88371
with #3264 applied 0.99982 0.99982 0.99982

(Forcing the split serially reproduces the mpirun numbers exactly; that was
checked against -np 1, 2, 4 and 8 on the pre-fix code.) CI runs its MPI jobs
at -np 2, so the checks pass here, but at four chunks the smoothing-on case
misses its 1% tolerance by an order of magnitude.

That is #2578 rather than a defect in this PR. The underlying gradient vector
is 12-44% off in L2 at every split above one chunk; the directional
finite-difference check largely misses this because it probes along g/|g|,
i.e. along the erroneous gradient itself. With #3264 in place the ratios are
identical at every chunk count, and the 1% tolerance in these tests could be
tightened again.

🤖 Generated with Claude Code

…ient consistency)

Fixes NanoComp#3258 and NanoComp#3259, with regression tests.

- get_uproj_w(): the 3D branch divided by 4 / 3 * pi * rad^3, where
  4 / 3 is integer division, so the smoothing kernel integrated to 4/3
  instead of 1 and the smoothed tensor was scaled everywhere the
  material grid has a nonzero gradient (interface-parallel components
  by 4/3, the perpendicular one by 3/4). Use 4.0 / 3.0. The 1D and 2D
  branches were already correct. (NanoComp#3258)

- eps_averaging=False was not propagated to the adjoint gradient:
  set_materials_from_geom_epsilon() stored the raw subpixel maxeval in
  geps even though structure_chunk::set_chi1inv() zeroes it when
  averaging is off, and the _set_materials Python wrapper left the
  header defaults in place when skipping set_materials_from_geom_epsilon
  entirely. The adjoint therefore differentiated a smoothed operator
  that the forward solve never used; measured directional
  finite-difference vs adjoint disagreement of ~70-100% in 3D with
  do_averaging=True and eps_averaging=False, which is the documented
  reason 3D adjoint users had to disable smoothing. (NanoComp#3259)

- New Simulation(allow_3d_subpixel=...) convenience switch to turn off
  MaterialGrid do_averaging for a 3D run in one place; the default True
  leaves behavior unchanged.

test_subpixel_3d.py checks the kernel normalization analytically in
1D/2D/3D and validates directional finite-difference vs adjoint
gradients in 3D for smoothing on, off, and the mixed
do_averaging=True/eps_averaging=False case (all now agree to ~1e-4).
@stevengj

Copy link
Copy Markdown
Collaborator

@smartalecH and @romanodev, can you take a look at this? Is it done correctly in the SSP repository?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants