Skip to content

Extremum Sum Potential - #259

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

fsichetti wants to merge 266 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

fsichetti and others added 12 commits August 19, 2026 18:21
This flag switched the high-order-contact collision building and
potential evaluation to an alternate per-vertex "feasible region"
code path replicating the OGC paper's algorithm directly, calling
into src/ipc/ogc/feasible_region.hpp. It was never enabled anywhere
(no caller set ogc_collisions=true), so this is dead-code removal,
not a behavior change for any existing user.

src/ipc/ogc/ (the standalone OGC implementation) is untouched --
verified via `git status --short src/ipc/ogc/` (clean) and the full
[ogc] test suite still passing (19 cases, 4147 assertions). The
[high_order_potential*] suite also passes unchanged (22 cases,
36559 assertions).
Reorders `#include <array>` into the correct IncludeCategories group
per .clang-format, as flagged by clang-format --dry-run --Werror.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	CMakeLists.txt
#	src/ipc/barrier/barrier.cpp
#	src/ipc/barrier/barrier.hpp
#	src/ipc/config.hpp.in
#	src/ipc/distance/distance_type.cpp
#	src/ipc/potentials/potential.cpp
#	src/ipc/smooth_contact/distance/edge_edge.cpp
#	src/ipc/smooth_contact/distance/edge_edge.hpp
#	src/ipc/smooth_contact/distance/point_edge.cpp
#	src/ipc/smooth_contact/distance/point_edge.hpp
#	src/ipc/smooth_contact/distance/point_face.cpp
#	src/ipc/smooth_contact/distance/point_face.hpp
#	tests/src/tests/barrier/test_barrier.cpp
#	tests/src/tests/benchmark_eigen.cpp
…gression from the merge

- Barrier classes became templates (default T=double) in the ipc-sim/main
  merge; ClampedLogBarrier/NormalizedClampedLogBarrier call sites in
  high_order_contact and barrier tests needed <> to keep compiling.
- edge_edge_distance_type's EA_EB near-degenerate fallback (normal-vector
  check + closest-point min) was templated on T but left hardcoded to
  Eigen::Vector3d; scope it to is_floating_point_v<T> so autodiff/SIMD
  scalars still compile.
- Rename the pre-existing geogram-based test oracle in
  tests/.../distance_type_reference.hpp (was distance_type_exact.hpp,
  now shadowed by the new production header of the same name in
  src/ipc/distance) and give it its own local PARALLEL_THRESHOLD, since
  production no longer exposes that as a global constant.
- edge_edge_distance_type is now unconditionally the thresholded analytic
  classifier (no more DistanceTypeConfig exact-predicate default), so
  update two tests whose assumptions predated that: compare against the
  reference using the same threshold instead of forcing an exact (0)
  threshold, and accept the edge-interior-vs-vertex classifications that
  are valid (and distance-preserving) for collinear degenerate edges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consistent naming for the two contact formulations:
  - ESP (Extremum Sum Potential): src/ipc/esp/, ESP* types, esp_* symbols
  - GCP (Geometric Contact Potential): src/ipc/gcp/, GCP* types, gcp_* symbols

Acronyms are capitalized in type names to match the convention already
used in this repo (LBVH, AABB, BVH, IPCWrapper); files, folders and
namespaces stay lowercase, matching src/ipc/ogc/ and namespace ipc::ogc.

Cosmetic only; no functional changes.

Deliberately left untouched:
  - The Ferguson2023HighOrderIPC citation and its prose in the docs.
    High-Order IPC is a different method from ESP.
  - Generic math helpers whose "smooth" is the ordinary mathematical
    sense (smooth_heaviside, smooth_clamp*, smooth_mu*, smooth_friction_*),
    which are shared with the friction code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the high_order_contact -> ESP rename: the arbitrary-point
evaluator is an ESP evaluation at an off-mesh point.

  ArbitraryPointPotential -> ArbitraryPointESP
  arbitrary_point_potential.{cpp,hpp} -> arbitrary_point_esp.{cpp,hpp}
  [arbitrary_point_potential] -> [arbitrary_point_esp]

Also capitalizes the ESP and GCP acronyms in type names (ESPPotential,
ESPParameters, ESPCollisions, GCPPotential, GCPParameters, ...) to match
the convention already used in this repo for LBVH, AABB, BVH and
IPCWrapper. Files, folders and namespaces stay lowercase, matching
src/ipc/ogc/ and namespace ipc::ogc.

Replaces the remaining "HO potential" comments with "ESP potential".

ArbitraryPointBVH is deliberately left alone: it is a general
broad-phase index wrapping ipc::LBVH, not ESP-specific.

Cosmetic only; no functional changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reformat the files flagged by the clang-format 21 check (the version
pinned in .github/workflows/clang-format-check.yml).

Most are fallout from the ESP/GCP rename: the longer type names
(HighOrderContactPotential -> ESPPotential, Esp* -> ESP*) shifted line
lengths past the column limit, and the new include paths re-sort
(ipc/gcp/... now precedes ipc/geometry/...). A handful of files under
esp/collisions/, math.tpp and gcp/distance/mollifier.tpp were already
failing before the rename and are fixed here too, since the rename had
already touched them.

Formatting only; no functional changes. Verified: full build clean and
the [esp_potential]/[gcp_potential]/[arbitrary_point_esp]/[smooth_clamp]
suites give byte-identical results (45,241 assertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	src/ipc/barrier/barrier.cpp
#	src/ipc/distance/CMakeLists.txt
#	src/ipc/math/CMakeLists.txt
fsichetti and others added 3 commits September 9, 2026 13:31
Three unrelated CI failures on ipc-sim#259:

1. Windows: near/far are macros in windef.h, so MSVC mangled
   NearFarBarrier's declarations ("error C2059: syntax error: 'const'").
   Renamed the two members to near_value/far_value, matching the existing
   first_derivative_near/far naming. This also broke polyfem's Windows
   build, which consumes this header.

2. Python (all 3 platforms) and CUDA: the public candidates.hpp included
   ipc/utils/unordered_map_and_set.hpp, whose own comment documents it as
   internal because Abseil is a private dependency. The Python bindings
   include candidates.hpp and do not link Abseil, so they failed with
   "absl/hash/hash.h: No such file or directory". Moved the nine
   unordered_map members into a pimpl defined in the .cpp, following the
   pattern upstream already uses: every other header including that file
   is a builder/details/internal header unreachable from python/src. The
   members were only ever used inside candidates.cpp despite being public,
   so no caller changes. The accessors return {} when the pimpl is null,
   preserving the previous empty-map behaviour.

3. Windows: geogram 1.9.8's vendored PoissonRecon includes <hash_map>,
   a pre-standard header removed from current MSVC, and it is compiled
   unconditionally (no GEOGRAM_WITH_* guard). geogram 1.10.1 drops
   Hash.h entirely, so bump to it. It no longer pulls predicates.h in
   transitively via exact_geometry.h, so include it explicitly where
   PCK::initialize is used.

Note IPC_TOOLKIT_WITH_GEOGRAM=OFF is not a viable alternative: it builds,
but 4 test cases fail because the *_exact distance-type classifiers fall
back to the analytic implementation.

Verified locally: [distance-type],[esp_potential],[gcp_potential],
[arbitrary_point_esp] pass (39 cases, 4610315 assertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
readability-braces-around-statements accounted for 136 of the 200
distinct clang-tidy errors on this branch. Applied via
clang-tidy --fix (18.1.8, matching the version CI installs).

Braces only: verified the token stream of every changed file is
unchanged apart from the added braces. Tests unaffected (51 cases,
4623026 assertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes the remaining readability-identifier-naming errors, plus
performance-enum-size, readability-named-parameter and
performance-unnecessary-value-param.

The config sets MemberCase: lower_case and its ignore patterns allow
*leading* underscores ('^(_.*)$'), so trailing-underscore names were
flagged. Upstream's convention for members is a leading m_ prefix
(e.g. CollisionMesh::m_full_rest_positions), so:

  mesh_            -> m_mesh          (Candidates)
  ptr_ / size_     -> m_ptr / m_size  (span)
  m_A / n_A_rows   -> m_a / m_n_a_rows (VertexMatrixView)
  use_standard_    -> m_use_standard  (DistanceTypeConfig)
  _dbar_factor     -> dbar_factor_value (public member)
  grad_P, P_near   -> grad_p, p_near  (local struct members)

Trailing-underscore constructor parameters and locals became leading
underscore, matching ParameterIgnoredRegexp/VariableIgnoredRegexp.
Global constants became UPPER_CASE. IntegrationType now has an explicit
std::uint8_t base. Unused override parameters are commented rather than
unnamed, and PointPotential takes ESPParameters by const reference.

span keeps its lowercase name via NOLINTNEXTLINE, since it deliberately
mirrors std::span -- the same escape hatch upstream uses in
default_init_allocator.hpp.

Verified: clang-tidy 18.1.8 (the version CI installs) reports no errors
for the previously failing translation units, and the tests are unchanged
(56 cases, 4623099 assertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zfergus zfergus added the enhancement New feature or request label Sep 11, 2026
@zfergus zfergus added this to the v2.0.0 milestone Sep 11, 2026
fsichetti and others added 12 commits September 16, 2026 11:03
test_avx_availability() emitted /arch:AVX2 and /arch:AVX from two
independent ifs, so a host detecting both got "/arch:AVX2 /arch:AVX".
/arch: is single-valued on MSVC and the last one wins, so every AVX2-capable
Windows build was compiled as plain AVX. CI reported this as
"SIMD support found: /arch:AVX2;/arch:AVX".

This is not only a performance loss. MSVC has no separate FMA flag -- the
FMA probe adds -mfma only on the GCC/Clang branch -- so /arch:AVX2 is the
only way MSVC enables fused multiply-add. Dropping to /arch:AVX removes FMA,
which changes rounding: a*b+c rounds twice instead of once, so results differ
from the AVX2 build rather than merely being computed more slowly.

Use if/elseif so AVX2 wins when detected, matching the AVX_VERSION selection
a few lines above, which already does this. AVX-only hosts still get
/arch:AVX and hosts with neither still get no flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in the GPU LBVH broad phase (ipc-sim#260), its profiling (ipc-sim#263), a
scalable_ccd pin bump (ipc-sim#261), and a docs/output fix (ipc-sim#264).

One conflict, in BroadPhase::detect_collision_candidates: upstream documented
that the candidates are cleared first, while this branch had added the
all_types parameter that ESP contact needs. The merged implementation already
contains both -- candidates.clear() and the all_types branch -- so the
declaration keeps our signature with upstream's wording, and documents
all_types, which had no @PARAM entry before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each predicate wrapper contained its exact-arithmetic fallback, which
kept it from being inlined, so every classification made one
out-of-line call per filter (~8-10) plus nested public point-edge calls.

- Move the exact fallbacks out of line so the filtered fast path of
  every predicate inlines into the classifier.
- Pass raw coordinate pointers and the dimension as a template
  parameter.
- Check DistanceTypeConfig and initialize geogram's PCK once per
  classification instead of once per nested point-edge test.
- Fix the trace message of the cross_dot_cross_2 fallback.

Same filters in the same order; classifications are bitwise identical.
Exact classification is 1.5-2x faster on real scenes (e.g., cloth-ball
edge-edge 80 -> 52 ns), with no measurable change at the ESP level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocation

Per-query allocation (a make_shared per collision, a hash map, a collision
dict with its own std::set/std::map, full-stencil gradient and Hessian
arrays) was ~30% of the run time of wildmeshing's smooth offset, which
evaluates this potential millions of times per run.

- Collisions are kept by value in per-thread lists, one per collision type
  (the types ESPCollisionDict stores), cleared between queries.
- The symbolic cancellation is unchanged: same sub-feature reduction, same
  distance-type classification, same integer-weight merge by typed hash,
  done by linear search over the few collisions of one type.
- Only the query vertex's block of each collision's gradient and Hessian is
  accumulated, instead of assembling the whole stencil and extracting it.

Measured on the previous base (3a76d75, before the rename): value,
gradient and Hessian agree with the old code to 3.6e-16 relative
(summation order), no NaN/inf, including points 1e-9 delta above shared
edges; ~2.4x faster per call; wildmeshing's cube offset run 456 s -> 337 s
with a bit-identical output mesh. On this branch: the six
[arbitrary_point_esp] tests pass; [esp_potential] fails the same 17
assertions with and without this commit (test data not downloaded here).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ESP sums b(distance to each element) with one weight per element. The
weights were fixed at the closed-mesh signs (3D: faces +1, edges -1,
vertices +1; 2D: edges +1, vertices -1), which are right only for a closed
manifold. On an open sheet the potential was exactly 0 beyond a boundary
edge, and an edge in no face or a 2D isolated vertex entered as a negative
barrier.

The weights now follow the counting rule of the ESP supplemental (S2): for
every point of the input, the weights of the elements containing it sum to
one. They are computed once from the connectivity:
- face: 1
- edge: 1 - (faces on it)
- vertex: 1 - (edges at it) + (faces at it)

This gives:
- closed mesh: +1/-1/+1 as before, so results there are unchanged
- boundary edge or vertex of an open sheet, end of an open polyline: 0 (S4)
- edge in no face: 1; vertex between two such edges: -1
- isolated vertex: 1
- edge shared by k faces, 2D vertex with k edges: 1 - k
Elements of weight 0 are skipped.

Tests: on an open sheet, an open polyline, a 3D polyline in no face,
isolated vertices and a 2D junction, the potential equals the barrier of
the distance, and on the sheet its gradient matches finite differences.
With the old weights the new test fails 18 assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The search-and-replace in the smooth_contact -> gcp rename rewrote
historical release notes ("Rename GCPPotential to GCPPotential") and
turned the Python naming test into a contradiction: it asserted that
GCPPotential both exists and does not exist.
5f301a2 dropped the std:: prefix, relying on ipc::unordered_map being
reachable through collision_mesh.hpp. It no longer is, so the Python
module failed to compile. Matches upstream again.
ESPCollisions stored its four maps as ipc::unordered_map members and
ESPCollisionDict::initialize took one as a parameter, so the public
esp_collisions.hpp included utils/unordered_map_and_set.hpp and with it
Abseil, a private dependency. tangential_collisions.hpp includes
esp_collisions.hpp, so every friction user needed Abseil on its include
path; the Python bindings failed to compile.

- ESPCollisions holds the maps in ESPCollisions::Maps, behind a
  unique_ptr and a maps() accessor, as Candidates does with
  AdjacencySets. Only library code used the maps.
- initialize() takes a named ESPCollisionPairMap, the same map type as
  before, so iteration order and results are unchanged.
- Both types are defined in the internal esp/esp_collision_maps.hpp.

Also drops an unused map.begin().value() that only compiled with
robin_map, a pair map keyed by std::array<int, 3> instead of index_t,
and a stray Abseil include in quadrature_potential.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bindings still targeted the old high-order contact API and no
longer compiled: they bound a use_adaptive_dhat overload of build(),
an operator[] ESPCollisions no longer has, the removed
QuadraturePotential class, and an ESPParameters constructor without
area_weights.

- ESPCollisions.build takes (mesh, vertices, param, broad_phase).
- ESPParameters takes area_weights, and its defaults now match C++
  (integration_type NORMAL).
- ESPPotential takes use_near_far.

Add Python tests for the parameter defaults, the gradient against
central finite differences, and the Hessian shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The smooth_contact -> gcp rename broke every downstream user of the
released names (e.g. polyfem uses SmoothCollisions,
SmoothContactParameters and SmoothContactPotential).

- C++: [[deprecated]] aliases SmoothContactParameters, SmoothCollisions,
  SmoothContactPotential, SmoothCollision and SmoothCollisionsBuilder.
- Forwarding headers at ipc/smooth_contact/smooth_collisions.hpp and
  ipc/smooth_contact/smooth_contact_potential.hpp.
- Python: SmoothCollision2, SmoothCollisions, SmoothContactParameters
  and SmoothContactPotential alias the GCP classes.

Replace the SmoothPotential naming test with a check of the aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove dead and experimental code:
- Remove ProfileRegistry. Nothing reads it, and the ESP Hessian locked
  its global mutex once per quadrature point.
- Restore gcp/ to main's content plus the deprecated aliases. The
  ESP-only distance helpers move to esp/esp_distance.hpp;
  half_edge_edge_mollifier had no callers. gcp_collisions_builder.cpp
  matches main again: upstream ipc-sim#188 had already reverted the by-value
  shared_ptr parameters.
- Remove ESP adaptive support (AdaptiveSupport, smooth_clamp and their
  tests). It stays on the esp-adaptive-support branch.
- Delete dead code: pair_distance.{hpp,tpp},
  smoothed_offset_potential_linear.h, math/span.hpp (ESPPrimitive gets
  vertex_id(i)), Math::log_barrier (superseded by ESPParameters::barrier
  in c02cc43), CollisionMesh::is_watertight() and its edge-face
  adjacency, whose non-manifold check could never fire, and ESP debug
  counters and helpers.

Fix:
- 3D ESP with default parameters (quad_order 1, empty face rule) built
  neither vertex nor face collisions, keeping only edge-edge contact:
  the build keyed on quad_order and the potential on face_quad_rule.
  Both now key on face_quad_rule; quad_order only sets the 2D rule.
- Revert the EA_EB parallel-edge fallback in core edge_edge_distance and
  the srand(0) added to its test. Its absolute 1e-20 threshold made d²
  26x too large for perpendicular edges 1e-6 long. ESP keeps the
  fallback in edge_edge_distance_parallel_safe.
- Candidates::build no longer copies the CollisionMesh; the
  adjacency-set queries take the mesh instead, and clear() resets them.
  ESPCollisions::build converts its own copy of the candidates instead
  of const_casting the caller's.
- Move ESP friction to esp/esp_tangential_collisions.cpp, and attribute
  edge-edge mollifier weights through edges_to_faces instead of
  scanning every face per pair.
- Only look up obstacle edges when skip_obstacles is set.
- Build: drop the duplicated CUDA block, the tests' Abseil/robin_map
  links and the Eigen recipe's EIGEN_DONT_VECTORIZE default (define it
  on ipc_toolkit with or without SIMD instead), and link geogram
  privately.
- Python: nest IntegrationType under ESPParameters and fix the ESP
  docstrings.

Tests:
- Restore the two commented-out codim normal-collision tests.
- Fix the FV mollification test's cube.obj path; it always skipped.
- Add tests for obstacle flags, skip_obstacles, the ESP integration
  types, BarrierPotential with unsquared distances, and the 3D ESP
  quadrature default.
- Test the exact distance-type classifiers against the reference
  (renamed *_reference), with 100k samples instead of 1M.
- Hide the 15-minute ESP "Expensive" test in every build, and drop the
  assertion-free "Number of Pairs" test and the benchmark_eigen
  experiment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Deprecated aliases for SmoothCollisionTemplate and
  TangentialPotential::smooth_contact_force{,_jacobian,_jacobian_unit}.
- Fix the warnings in the PR's code: lambdas captured structured
  bindings (a C++20 extension), an old-style cast, a switch without a
  default, and the assert-only total_p_far.

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.

5 participants