Skip to content

Replace python-fcl with Coal for aarch64 Linux support#440

Open
KE7 wants to merge 4 commits intoBerkeleyLearnVerify:mainfrom
KE7:coal-collision-backend
Open

Replace python-fcl with Coal for aarch64 Linux support#440
KE7 wants to merge 4 commits intoBerkeleyLearnVerify:mainfrom
KE7:coal-collision-backend

Conversation

@KE7
Copy link

@KE7 KE7 commented Mar 7, 2026

Summary

python-fcl does not provide Linux aarch64/ARM64 wheels on PyPI, making Scenic uninstallable on ARM64 platforms (NVIDIA Jetson, DGX Spark, AWS Graviton, Ampere servers, etc.). This PR replaces python-fcl with Coal — the actively-maintained successor to FCL.

Why Coal:

  • Pre-built wheels for all platforms including manylinux_2_28_aarch64 (PyPI)
  • Same collision detection model as FCL (BVH meshes, broadphase AABB trees, GJK/EPA)
  • BSD-3-Clause license (same as Scenic)
  • 5-15x faster for GJK-based queries (benchmarked by authors)
  • Actively maintained (554 stars, used by Pinocchio/HPP robotics frameworks)

What changed:

  • pyproject.toml: Replace python-fcl >= 0.7 with coal >= 3.0
  • src/scenic/core/regions.py: Use Coal API (Transform3s, BVHModelOBBRSS, CollisionObject, collide with request/result pattern). Replace trimesh.collision.CollisionManager (which internally used python-fcl) with direct Coal BVH checks.
  • src/scenic/core/requirements.py: Use Coal broadphase (DynamicAABBTreeCollisionManager with CollisionCallBackDefault)
  • tests/core/test_regions.py: Updated to use Coal API
  • docs/install_notes.rst: Removed obsolete python-fcl Apple Silicon build instructions (Coal installs cleanly everywhere)

Test results

Tested on aarch64 Linux (NVIDIA GB10, Python 3.12):

```
tests/core/test_regions.py: 164 passed, 217 skipped (ViewRegion slow tests)
tests/syntax/test_requirements.py: 46 passed, 1 xfailed
Full test suite (excl. simulators): 502 passed, 11 skipped
```

All collision-related tests pass identically with Coal, including:

  • `test_mesh_collision` — BVH surface intersection with rotated/translated meshes
  • `test_region_combinations` — all MeshVolumeRegion/MeshSurfaceRegion intersection combinations
  • `test_intersection_requirement` / `test_static_intersection_violation` — blanket collision requirements

Test plan

  • All existing collision detection tests pass with Coal on aarch64
  • `test_mesh_collision` verifies BVH construction and collision detection
  • `test_region_combinations` verifies MeshVolume/MeshSurface intersection
  • Blanket collision requirement tests pass
  • Coal installs cleanly via `pip install coal` on aarch64 Linux

python-fcl does not provide Linux aarch64/ARM64 wheels on PyPI, making
Scenic uninstallable on ARM64 Linux platforms (NVIDIA Jetson, AWS
Graviton, Ampere servers, etc.).

Coal (https://github.com/coal-library/coal) is the actively-maintained
successor to FCL with the same collision detection capabilities, BSD-3
license, pre-built wheels for all platforms including linux-aarch64, and
5-15x better performance for GJK-based queries.

Changes:
- Replace python-fcl dependency with coal>=3.0 in pyproject.toml
- Update regions.py to use Coal API (Transform3s, BVHModelOBBRSS,
  CollisionObject, collide with request/result pattern)
- Update requirements.py to use Coal broadphase collision manager
  (DynamicAABBTreeCollisionManager with CollisionCallBackDefault)
- Replace trimesh.collision.CollisionManager usage (which internally
  depended on python-fcl) with direct Coal BVH collision checks
- Update test_regions.py to use Coal API
- Remove obsolete python-fcl Apple Silicon install instructions from docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Eric-Vin Eric-Vin self-requested a review March 10, 2026 18:16
@codecov
Copy link

codecov bot commented Mar 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.63%. Comparing base (a97c99e) to head (9e448eb).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #440   +/-   ##
=======================================
  Coverage   89.63%   89.63%           
=======================================
  Files          48       48           
  Lines       13190    13204   +14     
=======================================
+ Hits        11823    11836   +13     
- Misses       1367     1368    +1     
Files with missing lines Coverage Δ
src/scenic/core/regions.py 87.93% <100.00%> (+0.08%) ⬆️
src/scenic/core/requirements.py 95.98% <100.00%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Collaborator

@Eric-Vin Eric-Vin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several initial issues with this PR:

  • It does not pass CI. Please ensure you are using the provided tools to meet the formatting guidelines and test changes locally with tox, as specified in the developer guide.
  • trimesh also uses python-fcl extensively, and we are heavily reliant on trimesh. Have you checked that we still don't inadvertently depend on python-fcl through our use of trimesh? Could you instead just make a request on the python-fcl repo for the necessary wheels? The maintainer tends to be fairly active there when it comes to wheel changes, and I would heavily prioritize a fix that doesn't bring us out of sync with trimesh.
  • In requirements.py you change the BlanketCollisionRequirement to use coal, but at first glance this update appears to, if there is a collision, re-compute which objects are colliding in a pairwise fashion? If this is correct this seems like it would add significant additional overhead to what is supposed to be a heuristically efficient check. Specifically, it seems like it would ruin the nice time complexity of BVH.

As a final note, please make sure that if you are using AI to generate your PRs you are reviewing what comes out. Reviewing is quite time intensive for the Scenic maintenance team and we would ideally like to spend that time reviewing code that has already passed initial muster.

- Remove unused `import trimesh` from requirements.py (dead code)
- Move CollisionRequest construction outside the pairwise loop
- Add comment explaining why pairwise identification is needed:
  Coal's Python bindings do not expose collision pair identity from
  the broadphase callback; the fallback only runs in the already-
  failing rejection path where a collision was detected
- Run black/isort on changed files to fix formatting CI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@KE7 KE7 closed this Mar 10, 2026
Coal's CollisionGeometry subclasses expose .id() (via IdVisitor) which
returns the stable C++ object address. This matches contact.o1.id() /
contact.o2.id() in collision results, giving O(1) pair identification
after the broadphase detects a collision.

The fix:
- Build geomIdToObj map keyed by collisionGeometry().id() before collide()
- Set num_max_contacts=1 on the broadphase request to capture a contact
- After collision, read contact.o1.id() / contact.o2.id() for O(1) lookup

This preserves the O(n log n) broadphase complexity end-to-end, with no
pairwise fallback loop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@KE7 KE7 reopened this Mar 10, 2026
@KE7
Copy link
Author

KE7 commented Mar 10, 2026

Thank you for the detailed review. We've addressed all three points.

1. CI / formatting

Black and isort have been run on all changed files. The PR now passes formatting checks.

2. trimesh and python-fcl

We investigated this carefully. `trimesh`'s dependency on `python-fcl` is optional and lazy: it lives in the `[recommend]` extras group and is guarded by `try: import fcl / except: fcl = None` at module load time. A plain `pip install scenic` does not pull in `python-fcl` through `trimesh`.

However, Scenic calls `trimesh.collision.CollisionManager` directly, and that class raises `"No FCL Available!"` at runtime when `python-fcl` is absent. This means ARM64 users who successfully install Scenic would hit a hard crash at first use rather than at install time — which is worse. Replacing those calls with Coal is therefore necessary to fully fix the problem, not incidental.

The rest of Scenic's `trimesh` usage (mesh loading, voxelization, proximity queries, ray casting) has no `python-fcl` dependency whatsoever and is untouched.

3. BlanketCollisionRequirement — O(n log n) preserved

The previous version of this PR introduced an O(n²) pairwise fallback to identify which objects collided after the broadphase, because we could not find a clean way to extract pair identity from Coal's callback. After deeper investigation of Coal's Python bindings we found the correct mechanism.

Coal applies `eigenpy::IdVisitor` to all concrete `CollisionGeometry` subclasses (`Box`, `Sphere`, `BVHModelOBBRSS`, etc.), which exposes an `.id()` method returning the stable C++ object address (`reinterpret_cast<int64_t>(&self)`). This same value appears on `contact.o1.id()` and `contact.o2.id()` in collision results, even though `contact.o1` otherwise returns a fresh boost.python wrapper on each access with no stable Python identity.

The implementation now works as follows:

# Build id → scenic_obj map before registering
geomIdToObj[collisionObject.collisionGeometry().id()] = obj

# Request one contact so the broadphase records the pair
callback.data.request.num_max_contacts = 1
manager.collide(callback)

# O(1) lookup — no pairwise loop
if callback.data.result.isCollision():
    contact = callback.data.result.getContact(0)
    self._collidingObjects = (
        geomIdToObj[contact.o1.id()],
        geomIdToObj[contact.o2.id()],
    )

The broadphase complexity is O(n log n) end-to-end. The `unused import trimesh` in `requirements.py` has also been removed.

Tests that falsifiedByInner correctly identifies the specific pair of
colliding objects via contact.o1.id() / contact.o2.id(), using three
objects where only two overlap, confirming the non-colliding object
is not reported and _collidingObjects is set to the right pair.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@KE7
Copy link
Author

KE7 commented Mar 10, 2026

Edge case: simultaneous multi-object collisions

If one object collides with two or three others at the same time, _collidingObjects reports one pair — whichever the broadphase AABB tree finds first. This is non-deterministic in that specific scenario but not a bug: falsifiedByInner still returns True correctly, and violationMsg only needs a colliding pair to display. The original fcl implementation had identical behaviour — it also read only contacts[0].

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.

2 participants