Skip to content

Extremum Sum Potential - #259

Merged
zfergus merged 331 commits into
ipc-sim:mainfrom
fsichetti:upstream-merge
Oct 21, 2026
Merged

zfergus merged 331 commits into
ipc-sim:mainfrom
fsichetti:upstream-merge

Conversation

@fsichetti

@fsichetti fsichetti commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds the Extremum Sum Proximity Potential (ESP) as a new ipc/esp/ module, with supporting changes to barriers, candidates, collision meshes, friction and distance-type classification; also renames smooth_contact to gcp with appropriate compatibility aliases.

Mechanism

  • ESPParameters: dhat, the near/far split parameter dbar_factor in [0, 2], the barrier (default NormalizedClampedLogBarrier), the 2D quad_order, an optional 3D face quadrature rule, area_weights and integration_type.
  • ESPCollisions: builds the quadrature collision set from broad-phase candidates.
  • ESPPotential: value, gradient and Hessian, with PSD projection.
    • 2D integrates over edges at Gauss-Lobatto points.
    • 3D integrates over faces (at their vertices or at face quadrature points) and at the closest points of nearby edge-edge pairs.
    • With use_near_far = true (the default), the 3D quadrature weights are normalized to sum to 1, split into near and far parts (NearFarBarrier) when dbar_factor > 0.
  • ArbitraryPointESP<dim>: value, gradient and Hessian of the potential at an arbitrary point, with inclusion-exclusion weights for open and codimensional input.
  • Friction: a TangentialCollisions::build overload that takes ESPCollisions; the existing FrictionPotential consumes the result.
  • Exact distance-type classifiers (point_edge_distance_type_exact, point_triangle_distance_type_exact, edge_edge_distance_type_exact) built on three shared sign predicates, so their classifications agree.

Impact

  • New optional dependency, geogram, on by default.
  • Breaking: the smooth_contact → gcp rename removes most old header paths (see below).
  • Additions to existing classes are defaulted, so existing calls compile unchanged.
  • Python exposes ESPParameters (without the barrier and face-rule settings), ESPCollisions, ESPPotential, the GCP* names and Candidates.build(..., all_types=False); the rest of the new API is C++ only.

API changes

New public types outside ipc/esp/

  • InversePowerBarrier(p): b(d) = h(d) / d^p, where h is a cubic B-spline that vanishes at dhat.
  • NearFarBarrier(base, alpha): splits a barrier into a near and a far part, with a smooth step between alpha * dhat / 2 and alpha * dhat.
  • DistanceTypeConfig, the *_distance_type_exact functions and is_parallel_edge_edge.

Additions to existing classes

  • Candidates::build(..., all_types = false) and BroadPhase::detect_collision_candidates(..., all_types = false): in 3D, all_types also collects vertex-vertex, edge-vertex, edge-face and face-face candidates, for ESP only.
  • Candidates: new ef_candidates, ff_candidates and adjacency sets (convert_candidates_to_sets(), vv_set() … ff_set()).
  • CollisionMesh: a constructor overload with an obstacle_vertex mask; is_obstacle_vertex/edge/face(); face_areas().
  • NormalCollisions::set_skip_obstacles(bool), default false.
  • BarrierPotential(..., use_squared_distance = true): false evaluates the barrier on the unsquared distance, where force_magnitude() throws.
  • CollisionStencil::name().

💥 Breaking changes

  • ipc/smooth_contact/ is renamed to ipc/gcp/, and its classes to GCP*.
    • Deprecated aliases keep the old class names compiling: SmoothCollision, SmoothCollisionTemplate, SmoothCollisions, SmoothCollisionsBuilder, SmoothContactParameters, SmoothContactPotential.
    • TangentialPotential::smooth_contact_force* is renamed to gcp_force*, with deprecated forwarders.
    • Python keeps the old names as aliases of the new classes, without a warning.
    • Only smooth_contact/smooth_collisions.hpp and smooth_contact/smooth_contact_potential.hpp remain, as forwarding headers; the other 15 old headers (and two .tpp files) are gone.
    • TangentialCollision::smooth_collision is renamed to gcp_collision, without an alias.
  • Code that takes the address of Candidates::build or BroadPhase::detect_collision_candidates with the old parameter list (e.g. py::overload_cast) must add the bool.

Known limitations

  • ESP friction does not support gcp_force_jacobian, always area-weights, and ignores the InversePowerBarrier power that ESPPotential applies to edge-edge weights.
  • Python cannot mark obstacle vertices, so integration_type has no effect there.

New dependencies

  • geogram 1.10.1: BSD-3-Clause, fetched with CPM, behind IPC_TOOLKIT_WITH_GEOGRAM (default ON).
    • Linked PRIVATE and used only in distance_type_exact.cpp; no public header includes it.
    • Library-only build (graphics, Lua, TetGen, Triangle and HLBFGS off); its own warnings are silenced on GCC and Clang.
    • geogram's default platform settings build it as a shared library on Linux and macOS, so executables and the ipctk module there need libgeogram at run time.
    • On Linux, geogram also links tbb, and OpenMP when built with GCC.
    • Wheels rely on cibuildwheel's repair step to bundle libgeogram; no PR check builds a wheel.
    • With the option OFF, the classifiers use floating-point predicates and log a warning once; no CI job builds this configuration.

Other changes

  • Fixes MSVC builds on AVX2 machines being compiled for AVX: FindSIMD.cmake no longer emits both /arch:AVX2 and /arch:AVX.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which improves existing functionality)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

New test files

  • test_esp_potential.cpp (30 cases, one hidden): FD derivatives, PSD projection, zero potential on closed meshes, bounded edge-edge terms near edge endpoints, and continuity in dbar_factor.
  • test_arbitrary_point_esp.cpp (8 cases): zero beyond dhat, FD derivatives, and inclusion-exclusion weights on open input.
  • test_obstacles.cpp (5 cases): obstacle flags, obstacle skipping in NormalCollisions, and ESP integration types.
  • test_vertex_matrix_view.cpp (5 cases).

New cases in existing files

  • test_distance_type.cpp (+6): four random tests (10,000 samples each, geogram builds only) compare the classifiers with an exact geogram reference; two check mutual consistency and the use_standard fallback.
  • test_force_jacobian.cpp (+4): ESP friction force against −∇D and friction Hessian against finite differences, in 2D and 3D.
  • test_candidates.cpp (+1, adjacency sets), test_barrier_potential.cpp (+1, unsquared distance), test_barrier.cpp (InversePowerBarrier sections).
  • Python: TestESPPotential and TestGCPAliases.

Reproduce

  • cmake --preset=test && cmake --build --preset=test -j 8 && cd build/test && ctest
  • pip install . && pytest python/tests/test_potentials.py

CI on 03c39d2

  • All 18 checks pass; patch coverage 96.57%.
  • Debug CTest time: 1010 s on Windows, 817 s on Linux, 869 s on macOS.
  • Slowest test: "ESP potential 2D finite differences", 885 s on Windows Debug (59% of the 1500 s timeout).

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

fsichetti and others added 30 commits January 26, 2026 16:19
…till has problems, could be numerical issues
zfergus and others added 26 commits October 18, 2026 07:39
- Take ConstRef in the ESP vertex helpers and builders so passing a Ref
  no longer deep-copies the whole vertex matrix on every call
- Make VertexMatrixView honor row/column strides so blocks, strided
  maps and row-major matrices read the right coordinates
- Reject view arguments that would need a temporary copy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Weight each edge and vertex by its valence instead of the closed-mesh
  signs, sharing the rule with ArbitraryPointESP
- Integrate the 3D potential over edges in no face and isolated vertices
- Build vertex collisions for every vertex near a primitive

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Build ESP dict DOFs in VERTEX_DERIVATIVE_LAYOUT order and pass the
  vertex count to every ESP Hessian scatter
- Hash integers, pairs and arrays generically in the non-Abseil fallback
- Make the ESP tests layout-aware

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keep NearFarBarrier's far part exactly 0 below alpha*dhat/2 so contact
  gives near = +inf instead of inf*0 = NaN
- Reject non-positive or non-finite NearFarBarrier alpha
- Return zero unsquared-distance barrier derivatives at d = 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Shrink ESPCollision::ELEMENT_SIZE from 120 to 12 DOF (Face3P1-Vertex3)
- Build near/far results in place instead of copying named locals
- Test that the ESP Hessian runs on a 512 KiB thread stack

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Solve with cross products so the denominator stays positive for every
  pair the parallel filter keeps
- Clamp the virtual vertex to the edge in the normal and friction builds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Make log_and_throw_error a template over the exception type
- Replace explicit throws with log_and_throw_error
- Treat edges and faces with any dynamic vertex as dynamic instead
  of throwing
- Check ee_set too when skipping 2D obstacle edges in NORMAL mode
- Never query obstacle flags under BRUTE_FORCE
- Throw std::invalid_argument for a wrongly sized obstacle mask

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ArbitraryPointBVH walked the tree with a 64-entry stack guarded only by
an assert, but a Morton-code tree can be up to 96 levels deep, so graded
geometry overflowed it in Release builds. Reuse
details::traverse_lbvh, whose stack fits the deepest valid tree and
throws on overflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ESP overload of TangentialCollisions::build took normalize_weights
in the slot where the normal and GCP overloads take blend_mu, so a
blend function passed positionally converted to true and was dropped.
A bool in that slot now fails to compile.
- to_string now lists the 2D edge quadrature collisions
- 2D builds now set num_quadrature_collision_pairs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Other translation units used the ESPCollisionTemplate specializations,
including the id-sorting vertex-vertex constructor, with only the
primary template visible, which is ill-formed (no diagnostic required).

- Declare every ESPCollisionTemplate specialization after the class
- Replace the two identical _near gradient specializations with one
  template and explicit instantiations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-collision overload (formerly smooth_contact_force_jacobian) was
declared but never defined. Define it as the Jacobian of the
per-collision gcp_force with the cached normal force held fixed, and add
the deprecated smooth_contact_force_jacobian alias.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add a scalar template parameter so both support float, double, and
  SIMD batches like the other barriers
- Hold the NearFarBarrier base in one shared_ptr, aliasing a borrowed
  raw pointer
- Add numext::pow and a SIMD lane-wise agreement test

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Replace the per-id std::set maps with compressed sparse row arrays
  built by a counting sort, and return sorted AdjList ranges instead of
  std::set copies from the Candidates::*_set queries
- Return views of the stored lists for ve_set and vf_set, which need no
  mesh topology and so allocate nothing
- Add a behavioral test of every adjacency query against a reference
  built from the candidate lists and the mesh
- Add an ESP benchmark that tracks the build, value, gradient, and
  Hessian cost of the lists

On cloth-ball (1 thread), ESPCollisions::build drops from 1027 to about
490 ms and the ESP value and gradient get about 3x faster.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Candidates::operator[] subtracts each list's size from the index as it
walks the candidate lists, so the out-of-range error printed the
leftover offset instead of the index the caller passed. Keep the
requested index for the message and update the two tests that still
expected the old message text.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Rename AdjacencyCsr to AdjacencyCSR and give Relation a char
  underlying type
- Define AdjacencyList after Candidates, forward declared above it, and
  move the adjacency code in candidates.cpp under its own section
- Query the built trees with a box around a point, so ESP no longer needs
  its own wrapper around LBVH
- Build ArbitraryPointESP's tree with zero inflation directly through LBVH

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@zfergus

zfergus commented Oct 21, 2026

Copy link
Copy Markdown
Member

Hi @fsichetti, I pushed a batch of commits directly to upstream-merge (6a059ee..9a4c264). It's in a good state now, so I'll merge it now. Here is a detailed list of the changes made.

💥 Changes that affect callers

  • geogram → MultiPrecision PSM: we now fetch only geogram's two-file exact-arithmetic module through CPM and build it as a static library in the ipc_geogram namespace, so it can coexist with a downstream geogram (e.g., PolyFEM).
    • IPC_TOOLKIT_WITH_GEOGRAM and the floating-point fallback are gone; the exact predicates always build.
    • There's no libgeogram shared library, TBB or OpenMP at run time, so wheels have nothing to bundle.
    • ExactVector3 replaces geogram's vec3E.
  • Candidates::*_set() now return AdjacencyList (a sorted, unique, contiguous range) instead of std::set<index_t>. ve_set/vf_set return views into the Candidates, so clear() or convert_candidates_to_sets() invalidates them.
  • ESP TangentialCollisions::build now takes blend_mu before normalize_weights, matching the normal and GCP overloads. Before, a blend function passed positionally silently converted to true; a bool in that slot now fails to compile.
  • Candidates::has_all_types records all_types. In 3D, ESPCollisions throws without it and NormalCollisions throws with it, since it would double count.
  • Removed ArbitraryPointBVH; ArbitraryPointESP uses the new LBVH::query_point(q, radius, ...).
  • log_and_throw_error<Exception = std::runtime_error>: explicit throws across src/ now log first.
  • Deprecated smooth_contact names now warn: the forwarding headers emit a compiler warning and the old Python Smooth* names raise DeprecationWarning.
  • The per-collision gcp_force_jacobian was declared but never defined. It now exists, with a deprecated smooth_contact_force_jacobian alias.
  • InversePowerBarrier and NearFarBarrier are templated on the scalar (float, double, SIMD batches) like the other barriers.

🐛 Bug fixes

ESP

  • Nearly parallel edges: the closest point is now solved with cross products, so the denominator stays positive for every pair the parallel filter keeps, and the virtual vertex is clamped to the edge.
  • Open meshes: edge/vertex weights now come from valence instead of closed-mesh signs. 3D now also integrates over edges in no face and over isolated vertices.
  • NaN at d = 0: the NearFarBarrier far part is exactly 0 below alpha*dhat/2 (avoids inf*0), and unsquared-distance barrier derivatives are 0 at d = 0.
  • ColMajor VERTEX_DERIVATIVE_LAYOUT: DOFs were in the wrong order. Builds without Abseil also failed to hash pairs and arrays.
  • Point-query stack overflow: ArbitraryPointBVH used a 64-entry stack guarded only by an assert, but an LBVH can be 96 levels deep. Graded geometry overflowed it in Release; we now reuse details::traverse_lbvh.
  • Stack size: ESPCollision::ELEMENT_SIZE drops from 120 to 12 DOF, and a test checks the Hessian on a 512 KiB thread stack.
  • Vertex matrix copies: passing a Ref deep-copied the whole vertex matrix on every call. ESP helpers now take ConstRef, and VertexMatrixView honors strides.
  • Obstacles: edges/faces with any dynamic vertex are now dynamic instead of throwing, and 2D NORMAL mode also checks ee_set. Flags are never queried under BRUTE_FORCE, and a wrongly sized mask throws std::invalid_argument.
  • ODR: ESPCollisionTemplate explicit specializations are now declared in the header.
  • 2D output: to_string and num_quadrature_collision_pairs now cover 2D. Also fixed a vacuous id assert in ESPCollisionDict.

Elsewhere

  • skip_obstacles weights: the skip now also covers the IMPROVED_MAX_APPROX corrections, vertex-vertex and plane-vertex pairs. Edge-edge/vertex-vertex pairs with one obstacle are weighted by the non-obstacle area instead of half the sum. This changes results when set_skip_obstacles(true).
  • all_types in 3D skips the codim. passes, whose candidates it already covers.
  • The Candidates::operator[] range error now reports the index you passed, not the leftover offset.
  • Fixed MSVC link errors against the PSM.

Validation

  • ESPParameters rejects non-positive or non-finite dhat and a negative quad_order; 2D needs quad_order >= 1.
  • ESPCollisions records its quadrature rule. ESPPotential and ESP friction throw on a mismatch instead of reading out of bounds.
  • InversePowerBarrier powers and NearFarBarrier alpha must be positive and finite.

⚡ Performance

  • Adjacency lists are now CSR arrays built by a counting sort instead of per-id std::set maps. On cloth-ball (1 thread), ESPCollisions::build goes from 1027 ms to about 490 ms, and ESP value and gradient are about 3x faster.

Tests & housekeeping

  • sphere.obj/wrapped_sphere.obj moved to ipc-toolkit-tests-data (tag bumped). A missing mesh now fails the test instead of crashing.
  • New tests:
    • adjacency queries against a reference
    • SIMD lane agreement for the templated barriers
    • layout-aware ESP
    • both sides of the edge-edge parallel threshold
    • obstacle cases
    • force Jacobians
    • VertexMatrixView strides
  • New [!benchmark] ESP adjacency benchmark.
  • The vendored Gauss-Lobatto code is relicensed under MIT to match Burkardt's current quad_rule, and the 2014 convergence check is ported.

PR description

Three parts of the description are now stale:

  • the geogram entry under New dependencies
  • "Python keeps the old names … without a warning"
  • the ESP TangentialCollisions::build parameter order

@zfergus
zfergus merged commit 7017fa9 into ipc-sim:main Oct 21, 2026
19 checks passed
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