Skip to content

Extremum Sum Potential - #259

Open
fsichetti wants to merge 293 commits into
ipc-sim:mainfrom
fsichetti:upstream-merge
Open

fsichetti wants to merge 293 commits into
ipc-sim:mainfrom
fsichetti:upstream-merge

Conversation

@fsichetti

@fsichetti fsichetti commented Sep 9, 2026 •

Copy link
Copy Markdown

Description

Implementation of the SigAsia paper "Extremum Sum Proximity Potential".

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How Has This Been Tested?

Built and ran the test suite locally on macOS/arm64.
This branch adds the following tests, all passing locally:

New test files

  • tests/src/tests/potential/test_esp_potential.cpp (19 cases, [esp_potential])
    ESP potential in 2D and 3D: convergent-quadrature edge-edge limits under
    three barriers, gradient/Hessian finite differences, Hessian PSD-ness,
    codimensional collisions, adaptive support, NearFarBarrier decomposition.
  • tests/src/tests/potential/test_arbitrary_point_esp.cpp (6 cases,
    [arbitrary_point_esp]) Evaluating the ESP at an arbitrary off-mesh point
    in 2D/3D: zero beyond dhat, FD gradient/Hessian, and that evaluate()
    agrees with operator()/gradient()/hessian().
  • tests/src/tests/potential/test_smooth_clamp.cpp (12 cases,
    [smooth_clamp]) smooth_clamp01 and smooth_clamp_simplex: anchor
    values, saturation, monotonicity, C0/C1 continuity at knots, FD-vs-analytic
    derivatives, and simplex partition-of-unity.
  • tests/src/tests/utils/test_vertex_matrix_view.cpp (5 cases,
    [vertex_matrix_view]) Non-owning two-matrix vertex view: concatenation
    against a naive vstack, aliasing semantics, empty-matrix edge case.

New cases in existing files

  • test_distance_type.cpp: four randomized tests comparing the analytic
    distance-type classifiers against a geogram exact-predicate reference
    (new distance_type_reference.hpp), including the near-parallel edge-edge case.
  • test_force_jacobian.cpp: ESP friction force-Jacobian in 2D and 3D
    ([friction-esp]).
  • test_barrier.cpp: log barrier derivatives.

Test Configuration:

  • OS and Version: macOS 15.5 (24F74), arm64
  • Compiler and Version: Apple clang 17.0.0 (clang-1700.0.13.5)
  • CMake 4.2.3, IPC_TOOLKIT_WITH_GEOGRAM=ON, IPC_TOOLKIT_WITH_CUDA=OFF

Checklist

  • I have followed the project style guide
  • My code follows the clang-format style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

claude added 8 commits October 5, 2026 17:32
- Define EIGEN_DONT_VECTORIZE only with IPC_TOOLKIT_WITH_SIMD again, as
  on main. Defining it unconditionally changed Eigen's object layout for
  every SIMD-off build (including the Linux Python wheels) and
  disagreed with MeshFEMSparse, which still defines it only with SIMD.
- Remove the ESPCollisionTemplate static_assert that required Eigen
  vectorization to be off. The ESP primitives hold no Eigen members,
  and the ESP tests pass with vectorization on.
- Default IPC_TOOLKIT_WITH_GEOGRAM to OFF, so the new dependency is
  opt-in. Without it, the distance-type classifiers use consistent
  floating-point predicates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
tests-data's cube.ply is the same unit cube as the cube.obj this PR
committed under tests/src/tests/potential/ (same vertices and triangles,
only the vertex and face order differ), so load it with
tests::load_mesh like the other tests and delete the copy. The old path
reached it through DATA_DIR / "../src/...", which only worked with the
default IPC_TOOLKIT_TESTS_DATA_DIR.

Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
ESP collisions encode their distance type in their class and assert it
when evaluated (Vertex2-Edge2P1 only at P_E), so evaluating the set
built at V at an FD stencil point across a switch aborted in Debug,
with or without geogram (mesh_1: a P_E pair 3.8e-10 inside its region,
FD step 1e-8).

- Gradient: rebuild the collisions at each perturbed configuration.
  The potential is C1 across switches, so the FD may cross one. mesh_1
  (880 DOFs) checks a directional derivative, as the 3D tests do,
  because a full FD with a rebuild per evaluation is too slow.
- Hessian: keep the FD of the analytic gradient on the set built at V.
  Rebuilding does not work there: the potential is only C1 across a
  switch, so a central difference averages two one-sided Hessians.
  Following the GCP tests, the Hessian is checked only where no stored
  P_E pair is within the FD step of a switch. Rounding leaves pairs
  about 1e-17 from one in the squares (order 14) and mesh_2 (orders 7
  and 14), so those sections check the gradient only.

Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
- Use the near/far split for every 0 < dbar_factor <= 2 (was (0, 1)).
  The far part of NearFarBarrier vanishes identically at 2, so the
  potential is continuous in dbar_factor, and dbar_factor = 1 is now a
  real split. The normalized unsplit branch is only reached at 0, where
  edge-edge terms are skipped, so the combined per-face PSD projection
  covers every case with indefinite weight-derivative terms.
- Cap the edge-edge weight support at dhat: the mollifier and the
  edge-edge cutoff use min(dbar, dhat) (ESPParameters::ee_support()),
  also in the tangential collisions.
- Keep edge-edge collision sets that cancel to empty, so their mollifier
  weight still counts in the face's weighted average; the potential
  jumped when a set changed between empty and non-empty.
- ESPParameters throws std::invalid_argument for dbar_factor outside
  [0, 2] and warns at 0. Default dbar_factor 1.0 -> 0.2 (C++ and Python).
- IPC_TOOLKIT_WITH_GEOGRAM defaults to ON again.
- Tests: dbar_factor range, NearFarBarrier far part at alpha = 2,
  continuity in dbar_factor at 1 and 2, cancelled edge-edge sets; both
  Hessian PSD tests also run on two-cubes-close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
- ESPParameters::face_quad_rule is now private. Set it with
  set_quad_rule(), which throws std::invalid_argument for a negative or
  non-finite weight, or for weights that sum to zero. A negative weight
  makes that point's barrier term unbounded below as it approaches
  contact (and broke the PSD projection of the per-stencil Hessian
  branches). get_quad_rule() is unchanged.
- Breaking: callers that assigned params.face_quad_rule must call
  params.set_quad_rule() instead.
- Remove the quad_order 6/8 and 10-12 log messages from ESPParameters:
  they describe a downstream triangle-rule table, while here quad_order
  only selects the 2D Gauss-Lobatto rule, whose weights are positive.
- Tests: rule validation; the 3D friction force-jacobian test uses
  set_quad_rule().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
- set_quad_rule() also throws std::invalid_argument for a negative or
  non-finite barycentric coordinate (point outside the face). Points
  whose coordinates do not sum to 1 (within 1e-10) only log a warning.
- Remove the quad_order > 14 check from ESPParameters: quad_order n
  selects the (n + 1)-point Gauss-Lobatto rule, which is tabulated up to
  19 points and computed beyond, and get_rule() already throws for
  n < 1 when the 2D rule is used. quad_order is unused in 3D.
- Tests: coordinate checks and the warning; Gauss-Lobatto weights are
  positive and sum to 1 for orders 1-25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
- Each rejected coordinate case now has exactly one bad coordinate, so
  +inf is tested on its own (it was previously rejected via -inf).
- Capture the expected warning by swapping the logger's sinks, so it no
  longer prints to stdout.

Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
Drop the infinite-coordinate case and the warning-capture block; keep
the negative and NaN coordinate cases.

Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.38366% with 131 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.08%. Comparing base (8a973ff) to head (493ff2f).

Files with missing lines Patch % Lines
src/ipc/esp/esp_tangential_collisions.cpp 90.11% 51 Missing ⚠️
src/ipc/esp/esp_potential.cpp 98.37% 16 Missing ⚠️
src/ipc/esp/quadrature_potential.cpp 98.08% 16 Missing ⚠️
src/ipc/esp/collisions/esp_collision_template.cpp 97.62% 11 Missing ⚠️
src/ipc/esp/collisions/esp_primitives.hpp 72.22% 10 Missing ⚠️
src/ipc/esp/collisions/esp_collision_dict.cpp 92.10% 6 Missing ⚠️
src/ipc/esp/collisions/esp_collision_template.hpp 73.68% 5 Missing ⚠️
src/ipc/esp/esp_parameters.hpp 91.48% 4 Missing ⚠️
src/ipc/gcp/collisions/gcp_collision.cpp 80.95% 4 Missing ⚠️
src/ipc/candidates/collision_stencil.hpp 0.00% 1 Missing ⚠️
... and 7 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #259      +/-   ##
==========================================
+ Coverage   96.93%   97.08%   +0.15%     
==========================================
  Files         194      221      +27     
  Lines       17349    22248    +4899     
  Branches      944     1540     +596     
==========================================
+ Hits        16817    21600    +4783     
- Misses        532      648     +116     
Flag Coverage Δ
unittests 97.08% <97.38%> (+0.15%) ⬆️

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.

claude added 8 commits October 7, 2026 22:56
- MSVC: the generic operator_nearfar, gradient_nearfar and
  hessian_nearfar were defined inline in the ESPCollisionTemplate class
  body, while the 3D versions are explicit specializations in the .cpp
  that the header does not declare. MSVC put the zero stubs in the
  vtable, so every ESP energy on the near/far path was 0.0 on Windows.
  Define the stubs out of line in the .cpp, as operator(), gradient()
  and hessian() already are.
- clang-tidy: ESPCollisions::build takes const ESPParameters&
  (performance-unnecessary-value-param); the Python binding's
  overload_cast follows.
- Debug timeouts: the Hessian PSD tests compute eigenvalues only on the
  DOFs with nonzero Hessian entries (the others are exact zeros).
  Convergent Quadrature Hessian PSD: 555 s -> 15 s locally in Debug.
- The face quadrature tests looped over quad_order, which 3D ignores.
  They now set positive-weight face rules with set_quad_rule(); the PSD
  test covers dbar_factor 1, 0.4 and 0 (48 runs instead of 96; 1691 s
  -> 46 s locally in Debug).
- Docs of ESPCollisions::build and the compute_minimum_distance
  docstring (squared distance) ride along in the touched files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
- Dependencies page: geogram row (IPC_TOOLKIT_WITH_GEOGRAM, BSD-3) and
  a note on the floating-point fallback; geogram node in
  dependencies.dot (dependencies.svg still needs regenerating).
- Barrier API reference: InversePowerBarrier and NearFarBarrier pages.
- NearFarBarrier: document alpha, the near/far split and the near/far
  methods.
- ESP doc comments: ESPCollision and ESPCollisionDict no longer
  describe GCP, fixed vertex counts or finish_insertion(); the barrier,
  area_weights, use_near_far, normalize_weights and the remaining
  build/hessian parameters are documented; stale comments about
  quad_order, Gauss-Lobatto point counts and "HOP" are fixed.

Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
The unsquared branches of operator(), gradient() and hessian() returned
the barrier without the stiffness factor, which upstream moved into
BarrierPotential. They now scale by stiffness() as the squared branches
do. The test runs with stiffness 1 and 3.5, with and without the physical
barrier, and checks that force_magnitude_gradient() throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
The continuous overload accepted all_types but never passed it to the
broad phase. Only the discrete overload, which ESP uses, needs it. The
signature and its Python binding match upstream again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
Fixes:
- The ESP TangentialCollisions::build never reset the lagged friction
  coefficients, which default to 0, so ESP friction was identically zero.
  It now calls reset_lagged_anisotropic_friction_coefficients() like the
  other overloads, and the friction test requires a nonzero force.
- ESPPotential::hessian returned a 0x0 matrix on a 3D mesh without faces.
- ESPCollisions::to_string read the face quadrature point, a virtual
  vertex, out of bounds. Face collisions now print ids and weights only.

Dead code (each removal verified against its callers):
- Edge-edge guards for non-EA_EB types and shared vertices, which the
  builder already filters out, become asserts. The ESP edge-edge distance
  helpers keep only the EA_EB case.
- Zero-weight fallbacks of the near/far normalization, the dbar_factor = 0
  edge-edge terms (the edge-edge pairs are skipped there), skips that
  ee_set/ef_set already guarantee, the obstacle-edge candidate check that
  was always true, the deep copies of the TBB exemplar and the unused copy
  assignment of QuadratureCollisionsBuilder, the uninstantiated
  Edge3P1-Edge3P1 distance and the ESPCollision near/far default bodies
  (now pure virtual).
- The 20-point Gauss-Lobatto rule now uses its table.

Tests: ESP friction with edge-edge contact, two cubes and zero stiffness;
obstacle integration types with a face rule, in 2D and with dynamic
vertex-vertex candidates; dbar_factor = 0; a 3D mesh without faces; 2D
Hessian projection; an edge-midpoint face rule; collision accessors and
stencils; distance helpers; candidate adjacency sets; and error paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
Rename the ESPParameters constructor parameter dbar_factor_value, which
shadowed the member of the same name, to _dbar_factor like the other
parameters, and use static_cast in ESPCollisionTemplate::vertex_id.

Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
Remove PointPotential::build_collisions_at_face_center and the gradient
and Hessian at the face center, with and without the near/far split.
Nothing in the library or the Python bindings called them; ESPPotential
uses the face-interior-point versions. The test that only checked them
against each other goes with them. The two value functions stay: they
evaluate the potential at every face quadrature point.

Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
evaluate_potential_at_face_center_with_cached_collisions and its _nearfar
version evaluate at any face quadrature point, not only the center. They
now match the gradient, Hessian and build functions, which already use
face_interior_point.

Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
@fsichetti
fsichetti marked this pull request as ready for review October 9, 2026 15:53
fsichetti and others added 3 commits October 9, 2026 11:54
The PSD check ran inside "ESP potential 2D finite differences" for every
configuration, including the 880-DOF mesh, and pushed that test past the
1500 s CTest limit in the Windows Debug job (it passed in 1115 s before).
It now runs on the two small configurations, in under a second.

Claude-Session: https://claude.ai/code/session_01R7sJzFKyZAgyK7X4Vs6mXf
zfergus and others added 6 commits October 12, 2026 18:44
The test timed out (1500 s) in the Windows Debug job in 4 of the last 5
runs; Linux and macOS Debug take about 850 s. Orders 1 and 14 keep the
two extremes (endpoints only, and the densest rule with points near the
endpoints); orders 2 and 7 stay covered by the 2D no-forces and Hessian
PSD tests. The test takes 59% of its previous time in a local Debug
build.
These new tests ran several times over one code path and made the Debug
CI jobs three times longer than on main.

- ESP potential 2D no forces: quadrature orders 1 and 14, and circles up
  to 200 vertices. The 2000-vertex circle took 97% of the test.
- Convergent Quadrature Gradient and Hessian: dbar_factor 0.3, 1.0 and
  2.0. dhat = 0.15 / min(dbar_factor, 1) keeps the edge-edge support at
  0.15; dividing by dbar_factor left no collisions at 2.0.
- Face Quadrature Gradient and Hessian: drop the interior-degree-2 rule,
  whose interior points the centroid of vertices+centroid already covers.
- Convergent Quadrature Hessian PSD: dbar_factor 0.4 only, since every
  value in (0, 2] takes the same projection path.
- Face Quadrature Hessian PSD: one rule, and dbar_factor 0.4 and 0. Zero
  is the per-stencil projection branch.
- Random distance-type tests: 10,000 samples instead of 100,000. The
  rarest type (edge-edge vertex-vertex, 2.5%) still appears about 250
  times per run.
- Prefix the non-public members of ESPPotential, ArbitraryPointESP,
  ArbitraryPointBVH, ESPCollisionDict and ESPCollisionTemplate with m_.
- Rename the getters ESPParameters::get_quad_rule() to quad_rule() and
  ESPPotential::get_use_near_far() to use_near_far().
- Make ESPParameters' dbar_factor private behind dbar_factor().
- closest_point_uv() takes ea0, ea1, eb0 and eb1.
- Document the ESPPotential and ESPCollisions constructors and maps().
- Remove the unused Vertex2ogc primitive, and the minimum-distance
  tracker that every collision evaluation updated with an atomic
  compare-and-swap but nothing read.
- Cast the six vertex-count asserts that MSVC flagged as signed/unsigned
  comparisons (C4389).
- Geogram compiles its sources with -Weverything on clang, which added
  about 3,000 warnings to the macOS builds. Build it with -w.
- Clear the link directory geogram adds from its source and relative
  build dirs. Under CPM that path does not exist, and the macOS linker
  warned about it.

This branch has not been deployed

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants