Skip to content

Fix EE-mollifier shape derivative (two bugs)#239

Merged
zfergus merged 1 commit into
ipc-sim:mainfrom
Huangzizhou:fix/ee-mollifier-shape-derivative
Jun 7, 2026
Merged

Fix EE-mollifier shape derivative (two bugs)#239
zfergus merged 1 commit into
ipc-sim:mainfrom
Huangzizhou:fix/ee-mollifier-shape-derivative

Conversation

@Huangzizhou

@Huangzizhou Huangzizhou commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two bugs in the per-pair NormalPotential::shape_derivative when collision.is_mollified() == true (near-parallel EE pair). Both surface only on the mollified branch, which IPC's existing shape_derivative test (stacked cubes, test_barrier_potential.cpp:248) does not exercise.

1. edge_edge_mollifier_gradient_wrt_x used the wrong second factor

Returns ∇_rest m = ∂m/∂ε · ∇_rest ε. The second factor was using edge_edge_mollifier_gradient(positions, eps_x) (returns ∇_position m) instead of edge_edge_mollifier_threshold_gradient(rest) (returns ∇_rest ε). Sister function edge_edge_mollifier_gradient_jacobian_wrt_x already used the correct factor.

 if (ee_cross_norm_sqr < eps_x) {
     return edge_edge_mollifier_derivative_wrt_eps_x(
                ee_cross_norm_sqr, eps_x)
-        * edge_edge_mollifier_gradient(ea0, ea1, eb0, eb1, eps_x);
+        * edge_edge_mollifier_threshold_gradient(
+              ea0_rest, ea1_rest, eb0_rest, eb1_rest);
 }

2. NormalPotential::shape_derivative was missing one outer-product term

The correct chain-rule expansion of ∇ₓ(f∇ᵤm + m∇ᵤf) — with ∇ₓm = ∇ᵤm + (∂m/∂ε·∇_rest ε), ∇ₓf = ∇ᵤf, ∇ₓ∇ᵤf = ∇ᵤ²f — has five terms:

    f · ∇ₓ∇ᵤm                                       (jac_m)
  + (∂m/∂ε · ∇_rest ε) · (∇ᵤf)ᵀ                   (gradx_m · gradu_fᵀ)
  + (∇ᵤf)(∇ᵤm)ᵀ + (∇ᵤm)(∇ᵤf)ᵀ                    symmetric pair
  + m · ∇ᵤ²f                                        (hessu_f)

The code summed only four (the (∇ᵤm)(∇ᵤf)ᵀ term was missing). That term is the "∇ᵤm contribution" of (∇ₓm)(∇ᵤf)ᵀ — its rest counterpart was being correctly accumulated into gradx_m · gradu_fᵀ, but its position counterpart was being forgotten.

-local_hess = f * jac_m + gradx_m * gradu_f.transpose()
-    + gradu_f * gradu_m.transpose() + m * hessu_f;
+local_hess = f * jac_m + gradx_m * gradu_f.transpose()
+    + gradu_f * gradu_m.transpose()
+    + gradu_m * gradu_f.transpose() + m * hessu_f;

Why it matters

In a polyfem shape-optimization transient adjoint, these two bugs combined produced a ~400× per-pair shape-derivative inflation at the touchdown frame and a ~10⁸× sign-flipped end-to-end gradient. The existing IPC shape_derivative test uses a non-mollified scene so neither bug appeared there.

Test plan

New regression test "Edge-Edge Mollifier Shape Derivative" in tests/src/tests/distance/test_edge_edge_mollifier.cpp constructs a minimal 4-vertex / 2-edge scene with a single mollified EE pair (y_sep = 9.99e-4 just inside dhat = 1e-3, z_twist = 2e-2 so |A × B|² ≪ eps_x). FD-checks the full 12×12 analytic shape_derivative against centered FD of gradient at fixed collision set with eps = 1e-7 (same convention as test_barrier_potential.cpp:377-391).

Frobenius rel
Without either fix ≥ 2.5 × 10⁻² (FAILS)
With only fix #1 ~5 × 10⁻⁵
With both fixes 3.5 × 10⁻⁶ (PASSES)
Threshold 1 × 10⁻⁵

~7000× discrimination, essentially FD precision after both fixes. All 122 existing mollifier-suite assertions still pass.

🤖 Generated with Claude Code

@Huangzizhou Huangzizhou marked this pull request as draft June 6, 2026 21:01
@Huangzizhou Huangzizhou force-pushed the fix/ee-mollifier-shape-derivative branch 4 times, most recently from 8e6cee6 to 174674f Compare June 6, 2026 23:46
@Huangzizhou Huangzizhou changed the title Fix edge_edge_mollifier_gradient_wrt_x to use ∇_rest ε_x Fix EE-mollifier shape derivative (two bugs) Jun 6, 2026
@Huangzizhou Huangzizhou marked this pull request as ready for review June 7, 2026 00:39
@Huangzizhou Huangzizhou force-pushed the fix/ee-mollifier-shape-derivative branch from 174674f to 8663296 Compare June 7, 2026 00:40
@zfergus zfergus requested a review from Copilot June 7, 2026 02:29
@zfergus zfergus self-requested a review June 7, 2026 02:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes two correctness bugs in IPC’s mollified edge-edge (EE) branch for NormalPotential::shape_derivative, and adds a regression test intended to exercise a near-parallel, mollified EE pair that existing shape-derivative tests do not cover.

Changes:

  • Fix edge_edge_mollifier_gradient_wrt_x() to use the correct threshold-gradient factor (rest-space) when differentiating through eps_x.
  • Fix NormalPotential::shape_derivative() (mollified branch) by adding the missing outer-product term ∇ᵤm (∇ᵤf)ᵀ.
  • Add a new FD regression test for mollified EE shape_derivative vs. centered FD of gradient.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/ipc/distance/edge_edge_mollifier.cpp Corrects the rest-space factor used in the mollifier gradient w.r.t. rest positions.
src/ipc/potentials/normal_potential.cpp Adds the missing symmetric outer-product term in the mollified shape_derivative expansion.
tests/src/tests/distance/test_edge_edge_mollifier.cpp Introduces a new regression test intended to validate mollified EE shape derivatives via finite differences.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/src/tests/distance/test_edge_edge_mollifier.cpp
@codecov

codecov Bot commented Jun 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.51%. Comparing base (9a704ea) to head (7b97031).
⚠️ Report is 16 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #239      +/-   ##
==========================================
+ Coverage   95.77%   96.51%   +0.73%     
==========================================
  Files         159      163       +4     
  Lines       16521    16651     +130     
  Branches      927      920       -7     
==========================================
+ Hits        15823    16070     +247     
+ Misses        698      581     -117     
Flag Coverage Δ
unittests 96.51% <100.00%> (+0.73%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…ᵤm)(∇ᵤf)ᵀ

Two bugs in the per-pair NormalPotential::shape_derivative when
collision.is_mollified() == true (near-parallel EE pair):

1. edge_edge_mollifier_gradient_wrt_x returns ∇_rest m, computed as
   ∂m/∂ε · ∇_rest ε. The second factor was using
   edge_edge_mollifier_gradient(positions, eps_x) (returns ∇_position m)
   instead of edge_edge_mollifier_threshold_gradient(rest) (returns
   ∇_rest ε). Sister function edge_edge_mollifier_gradient_jacobian_wrt_x
   already used the correct factor.

2. NormalPotential::shape_derivative (normal_potential.cpp:392)
   summed only four outer-product / Hessian terms in local_hess. The
   correct chain-rule expansion of ∇ₓ(f∇ᵤm + m∇ᵤf) — with
   ∇ₓm = ∇ᵤm + (∂m/∂ε·∇_rest ε), ∇ₓf = ∇ᵤf, ∇ₓ∇ᵤf = ∇ᵤ²f — has FIVE terms:

     f · ∇ₓ∇ᵤm                                      (jac_m)
     + (∂m/∂ε · ∇_rest ε) · (∇ᵤf)ᵀ                 (gradx_m · gradu_fᵀ)
     + (∇ᵤf)(∇ᵤm)ᵀ + (∇ᵤm)(∇ᵤf)ᵀ                   symmetric pair
     + m · ∇ᵤ²f                                     (hessu_f)

   The (∇ᵤm)(∇ᵤf)ᵀ term was missing; it is the "∇ᵤm contribution" of
   (∇ₓm)(∇ᵤf)ᵀ that gets split off from the rest contribution that goes
   into the gradx_m·gradu_fᵀ term.

Adds a regression test "Edge-Edge Mollifier Shape Derivative" in
tests/src/tests/distance/test_edge_edge_mollifier.cpp that constructs
a 4-vertex / 2-edge scene with a single mollified EE pair and FD-checks
the full 12x12 analytic BarrierPotential::shape_derivative against
centered FD of gradient at fixed collision set (same convention as
test_barrier_potential.cpp:377-391).

Without either fix, rel >= 2.5e-2. With both fixes, rel reaches FD
precision (~3.5e-6). Test threshold 1e-5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Huangzizhou Huangzizhou force-pushed the fix/ee-mollifier-shape-derivative branch from ee9b47a to 7b97031 Compare June 7, 2026 04:37
@zfergus zfergus merged commit b40e9c0 into ipc-sim:main Jun 7, 2026
18 checks passed
@Huangzizhou Huangzizhou deleted the fix/ee-mollifier-shape-derivative branch June 7, 2026 17:06
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.

issue in distance_type.tpp

3 participants