Skip to content

Add sparse meshless models (Faure et al. 2011); release 0.1.9 - #12

Merged
otmanon merged 7 commits into
mainfrom
claude/new-session-bvo8qi
Oct 7, 2026
Merged

otmanon merged 7 commits into
mainfrom
claude/new-session-bvo8qi

Conversation

@otmanon

@otmanon otmanon commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements Sparse Meshless Models of Complex Deformable Solids (Faure, Gilles, Bousquet, Pai, SIGGRAPH 2011) on triangle meshes with a per-element Young's modulus, following SimKit's flat one-function-per-module layout, and bumps the version to 0.1.9.

A compliance-distance metric (Dijkstra over mesh edges weighted by length / E) drives control-node placement, a Voronoi partition and material-aware skinning weights, which are then assembled into a sparse linear-blend-skinning subspace via a new sparse=True flag on the existing lbs_jacobian.

New public functions (each in its own module, exported from simkit)

Function Role
compliance_graph mesh-edge graph weighted by length / E (mean / min / max edge aggregation)
compliance_distances Dijkstra compliance distances from source vertices
compliance_node_sampling farthest-point sampling + Lloyd relaxation in the compliance metric (Sec. 4.4)
voronoi_labels nearest-node partition of vertices and triangles
voronoi_shape_functions material-aware, partition-of-unity, interpolating, compact-support weights W (Sec. 4.3)
sparse_meshless_methods_basis one-call entry point chaining the above

The coordinates that give every frame the same affine map [A | t] (the rest state, or a patch test) are the one-liner np.tile(np.c_[A, t].T.ravel(), k), documented on lbs_jacobian; no function is exported for it.

Changed

  • lbs_jacobian(V, W, sparse=False) gains an optional sparse flag. With sparse=True it assembles only the non-zeros of W and returns a scipy.sparse.csr_matrix with the identical DOF ordering as the dense result. The default dense path is unchanged, and both paths accept dense or sparse W.
  • skinning_eigenmodes(X, T, k, mu=...) now documents and tests its per-element mu (it was already forwarded to dirichlet_laplacian, but the signature and docstring said scalar). With the Lamé mu of a heterogeneous model the eigenmodes are material-aware: nearly constant on stiff parts, varying in soft ones. On the tutorial's tentacle they reach 96% of the full gravity sag with 9 bones, so they are the natural accuracy baseline for the sparse model, which trades accuracy per DOF for locality (compact support, sparse B, interpolating nodes).

Shape-function kernel (differs from the uploaded draft)

voronoi_shape_functions keeps the paper's clamped-linear tents in compliance distance (radius = distance to the nearest other node, so two frames blend linearly across a joint), but the radius is widened per vertex to d1 + d2 (the vertex's two nearest node distances) wherever the per-node radius would leave the vertex uncovered. The draft assigned such vertices rigidly to their nearest node, which made the weights jump across the Voronoi frontier in soft regions thicker than the node spacing and locked the joints of a thick model. The default support_scale is now 1.0: at 1.25 the forced exact interpolation at node vertices cut against the wider tents and produced the same kind of jump. On a 1.2-thick tentacle with 0.6-wide joints this raises the subspace's share of the full gravity sag from 44% to 62% at 9 frames and from 67% to 81% at 18.

Helpers nothing else uses are private to the module that owns them (_element_compliance, _farthest_point_in_compliance_metric, _lloyd_relaxation, _dense_lbs_jacobian, _sparse_lbs_jacobian), so they do not appear in the API reference.

Tests

One test file per module plus a new tests/conftest.py with shared stiff-bar strip fixtures. Coverage includes:

  • lbs_jacobian(sparse=True) equals the dense result to machine precision in 2D and 3D, for dense and sparse W, and its sparsity follows W
  • weights are non-negative, sum to one, interpolate exactly at node vertices, are linear between two nodes, cover every vertex, and are continuous across a frontier with unequal node spacing
  • affine patch test: the tiled [A | t] coordinates reproduce A x + t on the whole mesh through the full pipeline
  • crossing stiff material is cheap in the compliance metric; node sampling puts fewer nodes in the stiff bar than in the soft ends
  • seeds are kept first and fixed; results are deterministic
  • skinning_eigenmodes with a per-element mu gives modes that are flat on the stiff half of a strip and vary on the soft half

Full suite: 635 passed, 5 skipped (optional deps). python -m build + twine check pass, and sphinx-build generates an API page for each new module.

Tutorial

Tutorial 026 in simkit-tutorials (on main) walks through the pipeline on the tentacle and compares full FEM, uniform and material skinning eigenmodes and the sparse frames.

Release

pyproject.toml is bumped to 0.1.9 and CHANGELOG.md gains the 0.1.9 section. Once this is merged, pushing the v0.1.9 tag on main triggers release.yml and publishes to PyPI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UGZcwqzGYGz9YtjzzdLtBX

Otman Benchekroun and others added 7 commits October 7, 2026 21:11
… 2011)

Implements "Sparse Meshless Models of Complex Deformable Solids" on triangle
meshes with a per-element Young's modulus. A compliance-distance metric
(Dijkstra over mesh edges weighted by length / E) drives farthest-point node
placement with Lloyd relaxation, Voronoi labelling, and clamped-linear
material-aware skinning weights W. sparse_lbs_jacobian assembles W into a
sparse LBS subspace whose affine DOF ordering matches simkit.lbs_jacobian
exactly; lbs_affine_coordinates gives the reduced coordinates of a global
affine map for patch tests.

Exports the public functions from simkit, adds tests (dense/sparse basis
equivalence, partition of unity, interpolation at nodes, affine patch test,
material adaptivity of node placement) and a changelog entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGZcwqzGYGz9YtjzzdLtBX
Follows SimKit's flat layout: each public function lives in a same-named
module and is re-exported from the package root, with helpers that nothing
else uses kept private in the module that owns them.

  compliance_graph             (+ _element_compliance)
  compliance_distances
  compliance_node_sampling     (+ _farthest_point_in_compliance_metric,
                                  _lloyd_relaxation)
  voronoi_labels
  voronoi_shape_functions
  sparse_lbs_jacobian          (+ _point_frame_jacobian, _affine_frame_jacobian)
  lbs_affine_coordinates
  sparse_meshless_methods_basis

sample_nodes is renamed compliance_node_sampling so it is descriptive in the
flat simkit namespace; element_compliance becomes a private helper. Every
public function has a NumPy-style docstring so the autoapi reference picks
it up. Tests are split one file per module with shared mesh fixtures in a
new tests/conftest.py. Bumps the version to 0.1.9 and finalises the
changelog entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGZcwqzGYGz9YtjzzdLtBX
Replaces the separate sparse_lbs_jacobian module with an optional
sparse=False flag on lbs_jacobian. The dense path is unchanged; sparse=True
assembles only the non-zeros of W into a csr_matrix with the identical DOF
ordering. Point frames in sparse_meshless_methods_basis become the one-line
kron(W, I). Tests for the sparse path move into test_lbs_jacobian.py.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGZcwqzGYGz9YtjzzdLtBX
The per-node tent radius (distance to the nearest other node) leaves
vertices uncovered in soft regions thicker than the node spacing; the old
rigid nearest-node fallback made the weights jump across the Voronoi
frontier there, which locked the joints of a thick model. The radius is
now max(r_node, d1 + d2) per vertex, so the paper's linear tents are kept
wherever they cover and widened continuously elsewhere; the nearest node
always carries at least half the weight, no vertex is uncovered, and the
weights still interpolate the nodes exactly at support_scale = 1, which
is now the default (1.25 cut the forced interpolation against wider
tents and created the same kind of jump at node vertices).

Tests cover linearity between two nodes, coverage of far vertices,
continuity across a frontier with unequal node spacing, and the single
node case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGZcwqzGYGz9YtjzzdLtBX
mu was already forwarded to dirichlet_laplacian, which accepts one value
per element; the signature and docstring only advertised a scalar. State
the array form so heterogeneous (material-aware) eigenmodes are discoverable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGZcwqzGYGz9YtjzzdLtBX
The coordinates that give every frame the same affine map are
np.tile(np.c_[A, t].T.ravel(), k). Inline that in the tests and document it
on lbs_jacobian instead of exporting a function for it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGZcwqzGYGz9YtjzzdLtBX
@otmanon
otmanon merged commit 697f596 into main Oct 7, 2026
9 checks passed
@otmanon
otmanon deleted the claude/new-session-bvo8qi branch October 7, 2026 22:52
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.

1 participant