Skip to content

FEAT : Enable Riemannian metric-aware planning spaces#18

Merged
Kim-JeongHan merged 13 commits into
masterfrom
feat/rrtstar-r-planning-space
Jun 1, 2026
Merged

FEAT : Enable Riemannian metric-aware planning spaces#18
Kim-JeongHan merged 13 commits into
masterfrom
feat/rrtstar-r-planning-space

Conversation

@Kim-JeongHan

@Kim-JeongHan Kim-JeongHan commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add metric-space support to RRT-Connect, RRG, and PRM/PRM* configs so the RRT*-R terrain metric can be reused beyond RRTStar.
  • Extend examples/rrt_star_r_example.py with --planner {rrt-connect,rrg,rrt-star,prm-star} and --samples.
  • Replace the single RRT*-R README screenshot with a 4-image comparison table generated from Viser via Playwright.
  • Clarify README documentation by separating paper-driven RRT*-R features from repository-specific planner comparison additions.
  • Normalize remaining NumPy array conversions and remove the redundant RRT*-R minimal usage block.
  • Fix the pre-commit mypy failures by threading the validated diffusion training horizon through model construction and tightening NumPy scalar/grid typing.

RRT previously treated any node inside goal tolerance as complete, which could return paths without an exact-goal waypoint and could feed invalid best costs into Informed RRT*. The fix centralizes final-goal connection, delays parent attachment until collision checks pass, and rejects invalid planner step sizes before planning starts.
Rejected RRT-Connect extensions need to be visible, but representing them as normal child nodes breaks the Node parent/children invariant. Store rejected extension geometry as failed_edges instead, keep failed_nodes marker-only, and make the visualizer draw those explicit failed edges.
RRT*-R needs nearest queries, steering, edge costs, and collision sampling to use the same planning-space contract. Node-level Euclidean helpers made that boundary ambiguous, so graph-owned space operations now provide the geometry while Node remains tree data.

Terrain planning is exposed through a concrete Riemannian space and example so metric path costs and rendered waypoints can share edge-state samples.
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors the path planning library to support pluggable state-space metrics and edge geometry through a new PlanningSpace abstraction. The Graph class now delegates distance, steering, and collision checks to a configured space (defaulting to Euclidean). A new terrain module provides a procedurally-generated mountain with Riemannian metrics for geodesic-aware planning. All sampling planners (RRT, RRT*, RRT-Connect, PRM, RRG) are refactored to use the unified graph API. Supporting changes include informed sampler robustness, visualization updates, expanded test coverage, and a complete terrain planning example.

Changes

Graph-space abstraction and metric-aware planning

Layer / File(s) Summary
Planning space abstraction and interface
planning/space.py
Introduces EdgePath immutable container, abstract PlanningSpace class with distance/edge_states/edge_cost contracts and concrete steer/connect methods, and EuclideanSpace implementation using Euclidean geometry.
Graph and Node space integration
planning/graph/graph.py, planning/graph/node.py, planning/graph/__init__.py
Graph constructor accepts optional PlanningSpace parameter and delegates distance/nearest/near/steer/edge_cost/edge_states/is_edge_collision_free operations to space; Node removes distance_to methods, adds __lt__ for ordering; modules export only core Edge, Graph, Node types.
Procedural terrain and Riemannian planning space
planning/map/terrain.py, planning/map/__init__.py
Implements MountainTerrain with procedural height-map generation, bilinear interpolation, induced surface metric computation, and dense edge sampling; provides TerrainRiemannianSpace adapter; includes TerrainPlan data container and deterministic random start/goal generation with minimum-distance constraint.
Shared sampling planner base and goal connection
planning/sampling/base.py
Centralizes Graph ownership in RRTBase, adds _connect_goal helper for exact-goal nodes within tolerance, introduces _connection_radius abstraction for near-node selection in RRGBase, updates path-length computation via graph distance.
RRT and RRT-Connect core refactoring
planning/sampling/rrt.py (RRT / RRT-Connect sections)
RRT adds constrained Field defaults, uses graph-based nearest/steer/collision, routes goal connection through _connect_goal; RRT-Connect uses bidirectional Graph instances, explicit failed-edge tracking, and refactored extend/connect/swap logic.
RRT* and Informed RRT* with configurable space
planning/sampling/rrt.py (RRT*/Informed sections)
RRTStarConfig adds model_config and optional space parameter; RRTStar uses graph-based operations, explicit _connect_goal for exact goals, and graph methods for rewiring; InformedRRTStar mirrors updated flow with goal candidate accumulation.
PRM, PRMStar, and RRG graph-based operations
planning/sampling/prm.py, planning/sampling/rrg.py
PRM/PRMStar replace get_near_node overrides with _connection_radius method, use graph edge validation/cost; RRG refactored to use graph.nearest/steer/is_edge_collision_free throughout and graph.distance for goal checks.
Informed sampling numerical safety
planning/sampling/sampler.py
Updates InformedSampler.sample to check c_best finiteness and clamp ellipsoid radius; refactors ellipsoid scaling with explicit minor_axis and zero-norm guard; adds identity-matrix fast path in rotation computation.
Module exports, visualization, and camera utilities
planning/sampling/__init__.py, planning/visualization/rrt_connect_visualizer.py, planning/visualization/camera_utils.py
Sampling and graph modules export new space/terrain abstractions; RRT-Connect visualizer collects failed-edges via helper; camera utility accepts configurable elevation/azimuth parameters.
Test updates and new coverage
tests/test_node.py, tests/test_rrt.py, tests/test_diffuser_utils.py, tests/test_diffusion_dataset_source.py
test_node.py uses Graph instance methods; test_rrt.py expanded with test helpers (SequenceSampler, RejectAllPaths), parametrized config validation, custom-space delegation, terrain metric, rejection structure, close-start behavior, cost tracking, and informed sampler robustness tests; diffusion tests use explicit parameters.
Terrain-aware RRT* planning example and documentation
examples/rrt_star_r_example.py, examples/__init__.py, README.md
Provides complete terrain planning example with orchestrator, 3D projection, visualization, statistics, and CLI arguments; updates README with RRT*-R feature and detailed configuration; adds examples package docstring.
Diffusion training and checkpoint configuration
planning/diffusion/config/__init__.py, planning/diffusion/training/checkpoint.py
Adds default horizon value to DiffusionTrainingConfig; enhances CheckpointLoader.resolve to support numeric epoch strings.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 85.83% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately captures the main change: introducing Riemannian metric-aware planning spaces across the codebase, as evidenced by the new PlanningSpace abstraction, TerrainRiemannianSpace implementation, and metric-aware planner configurations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rrtstar-r-planning-space

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

The new RRT*-R path is easier to review and use when the README shows the planning-space boundary, terrain metric example, and headless smoke command alongside the existing planner examples.
@Kim-JeongHan Kim-JeongHan force-pushed the feat/rrtstar-r-planning-space branch from 4fc0498 to 4fdc7ea Compare June 1, 2026 03:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2eaaf5ba5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread planning/sampling/rrt.py
Comment thread planning/sampling/rrt.py
Numeric epoch strings now resolve to epoch checkpoint files, and training configs can defer missing-horizon validation until dataset preparation.
README now links the generated 500-iteration terrain-metric example image, and the terrain example uses a lower oblique camera angle for clearer documentation capture.
@Kim-JeongHan Kim-JeongHan changed the title FEAT : Enable metric-aware sampling without node-owned geometry FEAT : Enable metric-aware RRT* planning space Jun 1, 2026
Use rrt_star_r_example for the example script and docs image so the public example name matches the algorithm rather than the terrain demo domain.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (2)
tests/test_node.py (1)

93-95: ⚡ Quick win

Use Graph.add_node() here instead of mutating graph.nodes directly.

This test is supposed to validate the public Graph.nearest() contract, but assigning graph.nodes = nodes bypasses any invariants or indexes that add_node() may maintain. Building the graph through the public API keeps the test aligned with production usage.

Proposed test setup
     graph = Graph()
-    graph.nodes = nodes
+    for node in nodes:
+        graph.add_node(node)
     nearest = graph.nearest(target)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_node.py` around lines 93 - 95, Replace the direct mutation of
graph.nodes with using the public API: iterate the existing test's nodes
collection and call Graph.add_node(node) for each before calling
Graph.nearest(target) so any invariants or indexes maintained by add_node() are
populated; update references to the local variable names (graph, nodes, target)
and ensure the test still asserts the same nearest result after building the
graph via add_node().
tests/test_rrt.py (1)

507-510: ⚡ Quick win

Avoid hard-coding the 0.12 surface offset in this flat-terrain test.

The behavior under test is that both points project onto the same flat plane. Pinning the assertion to 0.12 makes this fail on harmless changes to the default lift used by states_to_surface_points().

Less brittle assertion
     points = terrain.states_to_surface_points(np.array([[0.0, 0.0], [1.0, 0.0]]))

     assert points.shape == (2, 3)
-    assert np.allclose(points[:, 2], np.array([0.12, 0.12]))
+    assert points[0, 2] == pytest.approx(points[1, 2])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_rrt.py` around lines 507 - 510, The test should not hard-code the
numeric surface offset; instead assert that both projected z-values are equal
(i.e., they lie on the same flat plane) and optionally that the offset is
sensible. Replace the fixed-value check with something like asserting
np.allclose(points[:, 2], points[0, 2]) (and if desired assert points[0,2] > 0
or compare against terrain.states_to_surface_points for a canonical single
point), locating the assertion near the existing call to
terrain.states_to_surface_points and using the points variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/rrt_star_r_example.py`:
- Around line 187-191: The current main() flow treats --save-image with
--no-show as a no-op and then falls into the interactive keep-alive loop; change
the control flow in main (the save_image and show handling) so that when
save_image is True and show is False the program captures/renders the image,
saves it to disk, cleans up any plotting resources (e.g., close figures) and
then returns/exit immediately instead of entering the interactive loop;
specifically update the branch that currently handles save_image/show to perform
the save-and-exit path (and remove the indefinite sleep/while loop for this
case) and ensure functions involved in rendering (the renderer/plotting calls
used in main) are invoked before exiting so the saved image is complete.

In `@planning/graph/__init__.py`:
- Around line 3-10: The PR removed public re-exports distance, get_nearest_node,
get_nodes_within_radius, and steer from planning.graph which is a breaking API
change; either restore them as deprecated aliases in planning/graph/__init__.py
that import and re-export the new implementations (e.g., add from .graph_impl
import distance, get_nearest_node, get_nodes_within_radius, steer and include a
deprecation warning on import) so callers continue to work for one release, or
if the removal is intentional, add a clear migration note in the project’s
changelog/release notes listing the removed symbols and their new
locations/usages (distance, get_nearest_node, get_nodes_within_radius, steer)
and mark them as removed.

In `@planning/map/terrain.py`:
- Around line 145-154: metric_h() computes interpolation weights tx/ty from
grid_x/grid_y which can be slightly outside [0, grid_size-1] near the
left/bottom world boundary, causing negative tx/ty and extrapolation; clamp
grid_x and grid_y into the valid range before computing col0/row0/tx/ty to
ensure get_height() samples stay on-grid. Specifically, after computing grid_x
and grid_y in the terrain sampling code, apply np.clip(grid_x, 0, self.grid_size
- 1) and np.clip(grid_y, 0, self.grid_size - 1) (or equivalent) before deriving
col0/row0/col1/row1 and tx/ty so metric_h() and get_height() never produce
negative interpolation weights.

In `@planning/sampling/base.py`:
- Around line 184-189: The helper currently returns neighbors from
self.graph.near(target, self._connection_radius()) but may include the target
node itself; update the method (the call site using self.graph.near and the
helper _connection_radius) to filter out the target before returning (e.g., call
self.graph.near(...), then return a list/iterable of neighbors excluding any
element equal to target using identity/equality), so PRM.plan() and RRG.plan()
will not receive a self-neighbor and won't create zero-cost self-edges.

In `@planning/sampling/sampler.py`:
- Around line 148-149: The expression computing minor_axis mixes a NumPy scalar
with a Python float which fails mypy; update the max call in the minor_axis
computation so both operands are plain Python floats (for example, cast the
result of c_best**2 - self.c_min**2 to float or use float(c_best)**2) before
passing to max, then keep scale_matrix construction unchanged; modify the
minor_axis and any dependent uses (the minor_axis = ... line referencing c_best
and self.c_min and the subsequent scale_matrix assignment) so mypy sees
consistent numeric types.

In `@planning/space.py`:
- Around line 37-49: edge_states() lacks a clear contract about the returned
array shape and endpoints, which causes Graph.is_edge_collision_free() to fail
if implementations return different layouts; update the docstring for
PlanningSpace.edge_states (and mention it in connect's docstring if desired) to
state it must return a numpy array of shape (N, node_dim) where N>=2 and
states[0] == start and states[-1] == goal (and that node_dim matches the input
vectors), and also note whether intermediate states are inclusive/exclusive of
endpoints and expected dtype; ensure edge_cost remains described as the cost for
that same ordered sequence so implementations must satisfy both contracts.
- Around line 26-35: steer currently uses Euclidean norm to cap motion but must
respect the space's planning metric; update steer to use the space's metric
(distance() or edge_cost()) instead of np.linalg.norm. Compute direction and do
a parametric interpolation new_point = start + t*(goal-start) and binary-search
t in [0,1] so that distance(start, new_point) (or edge_cost(start, new_point) if
that is the planner metric) is <= step_size and as close as possible; if
distance(start, goal) <= step_size return goal.copy() as before. Ensure you
still validate step_size > 0 and preserve return types and references to steer,
start, goal, step_size, distance()/edge_cost().

---

Nitpick comments:
In `@tests/test_node.py`:
- Around line 93-95: Replace the direct mutation of graph.nodes with using the
public API: iterate the existing test's nodes collection and call
Graph.add_node(node) for each before calling Graph.nearest(target) so any
invariants or indexes maintained by add_node() are populated; update references
to the local variable names (graph, nodes, target) and ensure the test still
asserts the same nearest result after building the graph via add_node().

In `@tests/test_rrt.py`:
- Around line 507-510: The test should not hard-code the numeric surface offset;
instead assert that both projected z-values are equal (i.e., they lie on the
same flat plane) and optionally that the offset is sensible. Replace the
fixed-value check with something like asserting np.allclose(points[:, 2],
points[0, 2]) (and if desired assert points[0,2] > 0 or compare against
terrain.states_to_surface_points for a canonical single point), locating the
assertion near the existing call to terrain.states_to_surface_points and using
the points variable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a46e5d3a-6537-4e22-88b1-ecdf9554069d

📥 Commits

Reviewing files that changed from the base of the PR and between fb89f56 and 40029de.

⛔ Files ignored due to path filters (1)
  • docs/images/rrt_star_r_example.png is excluded by !**/*.png
📒 Files selected for processing (23)
  • README.md
  • examples/__init__.py
  • examples/rrt_star_r_example.py
  • planning/diffusion/config/__init__.py
  • planning/diffusion/training/checkpoint.py
  • planning/graph/__init__.py
  • planning/graph/graph.py
  • planning/graph/node.py
  • planning/map/__init__.py
  • planning/map/terrain.py
  • planning/sampling/__init__.py
  • planning/sampling/base.py
  • planning/sampling/prm.py
  • planning/sampling/rrg.py
  • planning/sampling/rrt.py
  • planning/sampling/sampler.py
  • planning/space.py
  • planning/visualization/camera_utils.py
  • planning/visualization/rrt_connect_visualizer.py
  • tests/test_diffuser_utils.py
  • tests/test_diffusion_dataset_source.py
  • tests/test_node.py
  • tests/test_rrt.py
💤 Files with no reviewable changes (1)
  • planning/graph/node.py

Comment thread examples/rrt_star_r_example.py
Comment thread planning/graph/__init__.py
Comment thread planning/map/terrain.py
Comment thread planning/sampling/base.py
Comment thread planning/sampling/sampler.py Outdated
Comment thread planning/space.py
Comment thread planning/space.py
Add planner selection to the RRT*-R terrain example and document RRT-Connect, RRG, RRT*, and PRM* side by side with generated Viser screenshots.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
examples/rrt_star_r_example.py (1)

392-408: ⚠️ Potential issue | 🟠 Major

--save-image is still skipped when --no-show is set.

Line 392 returns before the save callback is registered, so --save-image --no-show silently does nothing. Either reject that combination explicitly or add a save-and-exit path instead of returning here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/rrt_star_r_example.py` around lines 392 - 408, The code returns
early when show is False, skipping registration of the save callback so
"--save-image --no-show" silently does nothing; fix by handling save_image
before the early return: if save_image is True ensure server is not None,
register the save callback (the handle_save function that calls save_docs_image
using PLANNER_IMAGE_NAMES[planner_name]) and arrange for the process to save and
then exit (or shut down the server) rather than returning immediately;
alternatively explicitly reject the combination by checking if save_image and
not show and raising a clear error—update the logic around the show/save_image
checks and the server.on_client_connect registration accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/rrt_star_r_example.py`:
- Around line 303-305: The startup banner is hard-coded to "RRT*-R" while the
script already computes planner_label via PLANNER_LABELS[planner_name]; update
the print statement to use planner_label instead of the fixed string (e.g.,
format the banner with planner_label so it reads "=== {planner_label}
Terrain-Metric Planning Example ===\n"). Ensure you reference the existing
planner_label variable (set by planner_label = PLANNER_LABELS[planner_name])
when composing the banner.

---

Duplicate comments:
In `@examples/rrt_star_r_example.py`:
- Around line 392-408: The code returns early when show is False, skipping
registration of the save callback so "--save-image --no-show" silently does
nothing; fix by handling save_image before the early return: if save_image is
True ensure server is not None, register the save callback (the handle_save
function that calls save_docs_image using PLANNER_IMAGE_NAMES[planner_name]) and
arrange for the process to save and then exit (or shut down the server) rather
than returning immediately; alternatively explicitly reject the combination by
checking if save_image and not show and raising a clear error—update the logic
around the show/save_image checks and the server.on_client_connect registration
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cae47c8e-1d62-4fca-bcdc-cb1b72615010

📥 Commits

Reviewing files that changed from the base of the PR and between 40029de and e43c6c0.

⛔ Files ignored due to path filters (4)
  • docs/images/rrt_star_r_prm_star_example.png is excluded by !**/*.png
  • docs/images/rrt_star_r_rrg_example.png is excluded by !**/*.png
  • docs/images/rrt_star_r_rrt_connect_example.png is excluded by !**/*.png
  • docs/images/rrt_star_r_rrt_star_example.png is excluded by !**/*.png
📒 Files selected for processing (6)
  • README.md
  • examples/rrt_star_r_example.py
  • planning/sampling/__init__.py
  • planning/sampling/prm.py
  • planning/sampling/rrg.py
  • planning/sampling/rrt.py
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • planning/sampling/prm.py
  • planning/sampling/init.py
  • planning/sampling/rrt.py

Comment thread examples/rrt_star_r_example.py
Separate the paper-driven Riemannian metric characteristics from the repository-specific planner comparison and visualization additions.
Commit the remaining cleanup changes that standardize array materialization and remove the redundant RRT*-R minimal usage block from the README.
Thread the validated diffusion training horizon through model construction and tighten NumPy scalar/grid typing so the pre-commit mypy hook passes.
Keep the diffusion package root importable without torch, and skip torch-dependent tests when the optional diffuser extra is not installed. Align the explicit CI typecheck step with the project pre-commit mypy hook so local and GitHub Actions checks use the same configuration.
@Kim-JeongHan Kim-JeongHan changed the title FEAT : Enable metric-aware RRT* planning space FEAT : Enable Riemannian metric-aware planning spaces Jun 1, 2026
@Kim-JeongHan Kim-JeongHan merged commit efc43eb into master Jun 1, 2026
2 checks passed
@Kim-JeongHan Kim-JeongHan deleted the feat/rrtstar-r-planning-space branch June 1, 2026 08:55
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