Repository navigation
Conversation
s_e used a_e = A/3 (faces) and L/2 (rods), which is only the measure per unit weight for vertex samples. With a face quadrature rule, whose weights sum to 1, that left EE samples about 3x heavier than intended. Use a_e = measure / (total weight of the element's fixed samples), so an EE sample's share on large elements is about m r^2 / A for any rule. Vertex-rule results are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Faces were weighted by area / 9 and edges in no face by length / 4, a leftover with no counterpart in the formulation, which integrates the point potential against the rest area. Weight each element by its rest measure instead: area for faces, edge area for edges in no face, vertex area for isolated vertices (unchanged). Energies of face-only meshes scale by 9, so barrier stiffnesses tuned with the old weights should be divided by 9; meshes mixing faces and rods also change the relative weight of the two. Update the edge-over-box transfer test values and the local-transfer doc's sweep numbers accordingly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the empty face rule ("order 0"), which integrated faces over their
vertices with weight 1 each, while explicit rules used their own weights.
The two conventions gave edge-edge samples different shares (up to 1/4 of
a face with vertex samples, 1/2 with a rule) and made the unnormalized
potential 3x the area integral for vertex samples only.
- Faces always use a rule, normalized to sum to 1. The default is the
vertex rule (corners, 1/3 each), the same as PolyFEM's order-1 rule.
An empty rule is rejected.
- Corner points are integrated with the shared vertex samples, weighted
by their rule weight, so each vertex is evaluated once instead of once
per incident face. Only the other points get per-face samples.
- Rods weigh 1/2 per endpoint and particles 1, so every element's fixed
samples weigh 1 and the unnormalized potential is the area integral.
- Local quadrature transfer accepts any rule with positive corner
weights: an edge's endpoints donate, capped by the smaller corner
weight, and the rule's other points are ordinary samples. With the
vertex rule this is the previous formula exactly.
- The prototype's area-consistent EE scale uses the element's measure
(its fixed samples weigh 1).
Tests: the no-EE normalized and unnormalized potentials now agree; the
edge-over-box legacy values and the EE endpoint-limit shift bounds are
re-measured; transfer derivatives are also checked with a rule that has
edge and interior points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ESPParameters::lattice_rule(m): the vertex rule on m x m congruent sub-triangles. Points on the lattice (i, j, k) / m with weights 1, 3 or 6 over 3 m^2 (corner, edge, interior): positive, corners and edge points included, spacing edge / m. m = 1 is the vertex rule. - ESPParameters::set_auto_lattice(spacing, max_subdivisions): each face uses lattice_rule(m) with m = ceil(equilateral side / spacing) from its rest area, clamped to [1, max]. The rule depends on rest geometry only, so it is fixed per face; it is not differentiable with respect to the rest shape, where a fixed rule should be used instead. - face_rule(rest_area) gives each face's corner weights and other points; the potential, collision building, friction, and local transfer use it. Collisions record the lattice settings, and evaluating them with different settings throws. - Python: set_lattice_rule(m), set_auto_lattice(spacing, max_subdivisions), lattice_spacing. Tests cover the lattice rules' weights, corners and first moments, the subdivision choice, auto vs fixed equivalence on equal-area faces, the settings check, and finite-difference gradients and Hessians with auto lattice rules for both the normalized potential and local transfer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The smoothstep EE mollifier faded samples in over w = sqrt(eps) d with the GCP eps = 1e-2, i.e. 0.1 d, while the edge's endpoint vertex loses its potential over the gap d as it rounds the other edge. The sample came in ten times faster than the vertex went out, so any residual energy step was released over a tenth of the gap: on the coarse box-crossing scene a 40 N lateral spike 1.5 um wide and a 2.4 N normal overshoot. The smoothstep-rel width is now w = sqrt(eps_rel) d with eps_rel = 1 (ESP_EE_MOLLIFIER_EPS overrides it; GCP keeps its eps). On the coarse box crossing the lateral peak drops to 3.9 N spread over 88 um and the overshoot to 0.6 N; on a refined mesh (body 8 x 8, target 32 x 32) the peak is 0.38 N with no overshoot. w = d is the widest relative width without a near-endpoint barrier hole (w <= d); the near-endpoint barrier map stays at 1.00 of a far crossing for gaps up to 100 um. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rmance Optimize ESP local transfer with inactive-source skipping and shared sample jets
Replace the prototype's environment variables (ESP_EE_MOLLIFIER, ESP_EE_MOLLIFIER_EPS/_ELL/_L, ESP_EE_AREA_REF, ESP_LOCAL_TRANSFER, ESP_EE_WEIGHT, ESP_MOLLIFIER_ORDER) with ESPParameters fields: - ee_mollifier (EEMollifierConfig: type, rel_eps, width, length), default SMOOTHSTEP_REL with rel_eps = 1 (w = d) - ee_area_scaling (default on) and ee_area_ref (0 = dbar) - ee_weight (1.5) and ee_mollifier_power (0 = from the barrier, see ee_mollifier_exponent()) - legacy_element_weights (default off): faces weigh area / 9 and edges in no face length / 4, as before the rest-measure change local_quadrature_transfer stays opt-in (off), since it throws for unnormalized weights and friction. The edge-edge mollifier functions take the config explicitly; the GCP formulation passes EEMollifierConfig::gcp(), so it keeps the original shape (under the env switches its values and analytic derivatives disagreed). ESP friction weights now match the potential's edge-edge weights: mollifier power and area scaling included. Old (env) and new (fields) evaluate identical energies and gradients (max relative difference 4e-16 over log and inverse-power barriers, normalized/unnormalized, with and without transfer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GCC rejects the single-brace forms under -Werror=missing-braces.
Tangential collisions now use each sample's coefficient in the transferred energy instead of rejecting transfer with friction: - a face's fixed samples keep w w_i (not normalized), and their far potentials keep it; - an EE sample's near potential gets sum_f w_f c_f s_f m_j / D_f, and the same mass leaves its source edge's endpoints' near coefficients in proportion (1 - u_j, u_j), so every coefficient stays nonnegative; - a sample's contact force is |w_near b_near'(d) + w_far b_far'(d)|. Faces only, as the rest of ESP friction. With no EE weight the friction matches the normalized formulation exactly (tested). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ESPPotential::set_lagged_weights() holds each integration element's near-weight total D_e (its sample weights plus its edge-edge mollifier weights, from element_near_weights() at a reference state) fixed. The energy is then sum_e w_e [N_e(x) / D_e + F_e(x) / W_e]: every sample enters with a fixed nonnegative coefficient, so - each vertex, face and edge-edge sample is evaluated once, not once per element containing it (about 6x for vertex samples); - each sample's Hessian is projected on its own stencil; the dense per-element block that the quotient rule needs for a PSD projection is gone. At the reference state the value equals the normalized energy; the derivatives omit the derivative of D_e. On the mat-twist state at t = 7 s (45k vertices), the projected Hessian takes 0.13 s with 1.22M nonzeros, against 2.17 s and 6.18M with local transfer and 1.18 s and 4.13M without. Not supported with local_quadrature_transfer (throws). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Build the nine candidate adjacency relations concurrently and sort and deduplicate their rows in parallel. Converting candidates to sets was two thirds of an ESP collision build (0.152 of 0.230 s on a 45k-vertex mat-twist state); it now takes 0.025 s, and the full build 0.113 s. - Add Candidates::has_sets(), and skip the conversion in ESPCollisions::build when the candidates already carry their sets (copies share them). - Add all_types to the swept Candidates::build, so ESPCollisions can be built from a line search's candidates at any point of its trajectory. - Bind the swept all_types build, convert_candidates_to_sets, has_sets, and ESPCollisions.build from candidates in Python. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With each element's denominator D_e held, element e's transferred term
w_e c_e s_e m_j (N(q_j) - R_j) / D_e is a fixed multiple of its pair's
term, and every element containing the source edge shares the pair. So each
pair is evaluated once, as
coef[ea] m_j (N(q_j) - (1 - u_j) N_a - u_j N_b),
coef[ea] = sum_{e ∋ ea} w_e c_e s_e / D_e,
and its Hessian is projected on the union of its closest point's, its
source endpoints', and its four vertices' stencils. The fixed samples keep
their per-sample path. element_near_weights() returns the transfer's
denominators (1 + sum_j s_e m_j, with its area scale) when transfer is on.
At the reference state the value equals the normalized transfer energy;
gradient and Hessian match finite differences of the lagged energy with
the projected Hessian PSD (log and inverse-power barriers, with and without
lattice rules).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With D_e frozen for a time step, an endpoint gives away coef * m_j * (1 - u_j) (or u_j) of near weight per edge-edge pair. That can exceed its fixed weight only if the element's edge-edge weight grew past its frozen value within the step. lagged_hessian() now logs, at debug level, how many endpoints donate, the lowest remaining fraction of fixed weight, and how many are negative. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ee_mollifier_exponent() now defaults to 1 for the smoothstep mollifiers and keeps round(p) + 1 under an InversePowerBarrier of power p only for the GCP family. GCP's weight starts linearly in the squared endpoint clearance, and without the power its gradient in the edge-edge endpoint-limit test reaches ~7e15 (p = 2). Smoothstep starts cubically and stays bounded (1.8e6 with or without the power), while the power also raises the distance spline to p + 1 and makes edge-edge contact up to 180x softer than vertex contact at moderate gaps (p = 2; 5x without). An explicit ee_mollifier_power still applies to any mollifier. The inverse-quadratic endpoint-limit thresholds are re-measured (shift of about 5e-5 of P and 5e-4 of |grad|). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - python ESP bindings: upstream split define_esp_parameters out of define_esp_potential; the new ESPParameters fields go in the former. - Candidates::build (CCD): upstream's thickness-aware broad-phase build with our all_types detection. The CCD build now also skips the codim. passes under all_types (3D), as the static build does, since they would replace the whole-mesh vertex-vertex and edge-vertex lists. Full non-benchmark test suite passes (455 test cases). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the fixed-width and sqrt-of-gap variants (SMOOTHSTEP with a fixed width, SMOOTHSTEP_SQRT, GCP_FIXED, GCP_SQRT) and their width/length fields: they were explored while diagnosing the edge-edge force spikes and either leave a hole in the barrier near edge endpoints or release the energy step too abruptly. What remains is the smoothstep mollifier with w^2 = rel_eps d^2 (default, w = d), renamed from SMOOTHSTEP_REL to SMOOTHSTEP, and GCP's original mollifier. Also document local_quadrature_transfer, clear_lagged_weights and lagged_weights in the Python bindings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add EEMollifierType and EEMollifierConfig to the ESP API pages, and describe the friction weights under local quadrature transfer in TangentialCollisions::build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- esp_local_transfer.cpp: braces around single-statement bodies, auto* for pointers, and rename a lambda's local that shadowed `out` (-Wshadow). - esp_local_transfer.hpp: name the declarations' parameters. - mollifier.tpp: braces around single-statement bodies. - esp_tangential_collisions.cpp: write the contact-distance test without the negation (still false for a NaN distance). clang-tidy (the project's configuration) now reports nothing on the changed files, as on main, and GCC reports no warnings in src/, tests/ or the Python bindings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…itives With all_types the whole-mesh pass already finds every vertex-vertex and edge-vertex candidate; the codim.-only passes clear their output first, so running them would leave only codim. pairs. Both builds skip them; the test covers both and fails for the CCD build without its guard (surface vertex-vertex and edge-vertex pairs drop to 0). Also clarify the comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local-only notes and example are ignored through .git/info/exclude instead.
…gging
Each face corner now gives half its weight to each of its two edges, and
the edge-edge samples on an edge take weight only from that edge's halves,
with one live normalization per (face, edge): coefficient c s with
c = min(w_a, w_b) / 2 (1/2 on rods) and denominator 1 + s sum m_j.
Samples on different edges are independent and a corner always keeps part
of its weight, so the energy is exact (no lag) and every coefficient is
positive:
E = sum_e w_e [sum fixed samples (N + F)]
+ sum_edges sum_pools (w c s) sum_j m_j (N(q_j) - (1 - u_j) N_a - u_j N_b)
/ (1 + s sum_j m_j).
Vertex and face-rule interior samples have constant coefficients, so each
is evaluated once and projected on its own stencil; each edge's edge-edge
term is one block over its pairs' and endpoints' stencils. This gives the
sparsity lagged normalization was added for, so lagged normalization is
removed (ESPPotential::element_near_weights, set_lagged_weights,
clear_lagged_weights, lagged_weights and their bindings). Friction under
transfer uses the same per-(face, edge) pools.
Also:
- lattice_subdivisions(): clamp before converting to int (a tiny spacing
overflowed).
- ESPPotential: log the transfer precondition error.
- Documentation: barrier power (GCP only), legacy_element_weights caveat,
ee_weight, ee_area_scaling's share c, Candidates::has_sets().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It only scaled face weights by 1/9 and rod weights by 1/4, which on a face-only mesh is a stiffness change, and it did not restore earlier energies anyway (corner weights now sum to 1). Also removes its test, binding and element_weight_scale().
Takes barycentric points (n x 3) and weights; Python could only set lattice rules before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #288 +/- ##
==========================================
+ Coverage 96.77% 96.95% +0.18%
==========================================
Files 224 226 +2
Lines 22772 23400 +628
Branches 1576 1671 +95
==========================================
+ Hits 22038 22688 +650
+ Misses 734 712 -22
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix ESP edge-edge force spikes on coarse meshes
Description
Fixes the large, narrow force spikes ESP produces when an edge-edge (EE) contact slides past an edge endpoint on coarse meshes (#280).
Also includes miscellaneous minor improvements.
Touches
src/ipc/esp, the EE mollifier insrc/ipc/gcp/distance, ESP friction, and the candidate build.Fixes #280
ee_area_scaling, on): each EE sample is weighted to stand for a (dbar x dbar) patch instead of a fixed share of its element.EEMollifierType::SMOOTHSTEP, default): C² fade-in over a width equal to the gap. GCP's original mollifier remains available.local_quadrature_transfer, off in ipc-toolkit): an EE sample steals weight only from its own edge's endpoints instead of renormalizing the entire element, which keeps the normal force steady while the sample fades in. Each face corner gives at most half its weight to each of its two edges, with one normalization per (face, edge), so samples on different edges stay independent. Supported with friction.all_types, so a simulator can build ESP collision sets at line-search trial points from the step's candidates instead of running a broad phase per trial point, as it already does for IPC; candidate-to-set conversion runs in parallel. Speeds things up a bit.set_auto_lattice): an optional fixed face quadrature rule, chosen per face from its rest area, that places more points on larger faces.API changes
ESPParameters, new optional fields:ee_mollifier(EEMollifierConfig:SMOOTHSTEPwithrel_eps = 1, orGCP),ee_mollifier_power(0: none for smoothstep, round(p) + 1 for GCP under an inverse-power barrier),ee_weight,ee_area_scaling,ee_area_ref,local_quadrature_transfer; face rules viaset_quad_rule()(weights summing to 1),lattice_rule(m)andset_auto_lattice().Candidates::build(..., all_types)for both the static and the swept build;has_sets().TangentialCollisions::build()for ESP supports local transfer.set_quad_rule(points, weights),set_lattice_rule(m),set_auto_lattice()).💥 Behavior changes
set_quad_rule({})(formerly vertex integration) now throws; uselattice_rule(1)or the default.test_esp_potential.cppwere re-measured.No new dependencies.
Type of change
How Has This Been Tested?
python/tests/test_esp_local_transfer.py.feature/esp-ee-fix-prototypebuilds against this and runs.Test Configuration:
Checklist
Results
ablation.mp4