Skip to content

Improve SRS IK arm-angle search and CPU/CUDA parity - #548

Merged
chase6305 merged 12 commits into
mainfrom
cjt/main/fix_srs_solver
Aug 28, 2026
Merged

Improve SRS IK arm-angle search and CPU/CUDA parity#548
chase6305 merged 12 commits into
mainfrom
cjt/main/fix_srs_solver

Conversation

@chase6305

@chase6305 chase6305 commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR improves the correctness and performance of the 7-DoF SRS analytical IK solver.

The reference plane and seed redundancy are now calculated from the actual shoulder-elbow-wrist geometry. Seeded search starts from the seed’s geometric arm angle and expands outward, while full search covers the complete arm-
angle range.

CPU and CUDA implementations now use consistent arm-angle, periodic joint-distance, and joint-limit handling. The implementation also reduces repeated CPU geometry calculations, removes unnecessary CUDA combination buffers, reuses
Warp temporary arrays, and improves all-solution sorting.

Tests were added for geometric arm-angle sampling, periodic joint wrapping, runtime TCP/weight updates, and CPU/CUDA parity. A benchmark was also added for randomized, boundary, near-singular, and unreachable targets.

No new dependencies are required.

Fixes #N/A

Type of change

  • Bug fix
  • Enhancement
  • Documentation update

Screenshots

python scripts/tutorials/sim/srs_solver.py --device cuda --num-steps 100
srs_solver-2026-08-25_20.23.32.mp4

Checklist

  • I have run the formatter on the changed files
  • I have made corresponding changes to the documentation
  • Public API changes are reflected in the API docs, if applicable
  • I have added tests that prove my fix is effective
  • Dependencies have been updated, if applicable (no new dependencies)

@chase6305
chase6305 requested review from yuecideng and a lite review from Copilot August 24, 2026 12:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates EmbodiChain’s 7-DoF SRS analytical IK solver to improve correctness and performance, with a focus on CPU/CUDA parity for redundancy (arm-angle) sampling, periodic joint handling, and solution ranking/deduplication.

Changes:

  • Added geometric arm-angle computation and a seeded/full redundancy search mode, aligning CPU and CUDA behavior.
  • Standardized periodic joint wrapping and nearest-solution distance metrics across CPU and CUDA backends.
  • Added new solver tests (sampling, wrapping, runtime updates, parity) and a dedicated benchmark script for representative workloads.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/sim/solvers/test_srs_solver.py Adds coverage for seeded redundancy sampling, periodic wrapping, runtime cache sync, and CPU/CUDA parity.
scripts/benchmark/robotics/kinematic_solver/srs_solver.py Introduces an SRS benchmark harness for latency/throughput and solution-quality metrics across scenarios.
embodichain/utils/warp/kinematics/srs_solver.py Updates Warp kernels for parity (arm-angle kernel, periodic wrapping, FK fix, combination indexing removal).
embodichain/lab/sim/solvers/srs_solver.py Implements new search modes, geometric seed arm-angle logic, periodic wrapping, deduplication, and runtime TCP/weight synchronization.
agent_context/topics/ik-solvers/ik-solvers.md Documents the updated SRS solver behavior, new settings, and benchmark location.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread embodichain/lab/sim/solvers/srs_solver.py Outdated
@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown

Greptile Summary

The PR improves the SRS analytical IK solver’s arm-angle search, geometric reference-plane calculation, periodic joint handling, and CPU/CUDA consistency.

  • Adds seeded and full-circle redundancy sampling with validation and complete fallback behavior.
  • Aligns CPU and Warp geometry, singularity handling, joint wrapping, ranking, and solution deduplication.
  • Synchronizes runtime TCP and nearest-weight updates and reuses CUDA temporary buffers.
  • Adds focused correctness, parity, and benchmark coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
embodichain/lab/sim/solvers/srs_solver.py Implements validated redundancy sampling, geometric arm-angle calculation, periodic solution handling, runtime cache synchronization, and aligned CPU/CUDA result processing.
embodichain/utils/warp/kinematics/srs_solver.py Updates Warp kernels to match CPU reference-plane, arm-angle, singularity, joint-limit, and combination-index behavior.
tests/sim/solvers/test_srs_solver.py Adds regression coverage for geometric sampling, complete fallback grids, invalid steps, periodic limits, runtime updates, singularities, and backend parity.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Seed[Seed joint state] --> Geometry[Compute geometric arm angle]
  Geometry --> Mode{Search mode}
  Mode -- Seeded --> Radial[Generate radial offsets]
  Radial --> Complete{Requested count reached?}
  Complete -- No --> Uniform[Replace with seed-centered uniform grid]
  Complete -- Yes --> Samples[Redundancy samples]
  Uniform --> Samples
  Mode -- Full or all solutions --> Full[Generate full-circle grid]
  Full --> Samples
  Samples --> IK[Evaluate CPU or CUDA IK branches]
  IK --> Limits[Wrap periodic joints into limits]
  Limits --> Rank[Rank and deduplicate valid solutions]
Loading

Reviews (12): Last reviewed commit: "save to ik_nearest_weight_tensor" | Re-trigger Greptile

Comment thread embodichain/lab/sim/solvers/srs_solver.py
Copilot AI review requested due to automatic review settings August 24, 2026 13:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (3)

Previously missed (2) — in code that hasn't changed since the last review.

embodichain/lab/sim/solvers/srs_solver.py:774

  • CPU get_ik converts xpos to NumPy inside the innermost candidate-solution loop (target_np = xpos.detach().cpu().numpy()), even though target_xpos_np[target_idx] is already available and constant for the target. This adds avoidable overhead in the tight IK search loop; compute the NumPy target once per target_idx (or reuse target_xpos_np[target_idx]) and reuse it for all candidates.
                    if success:
                        fk_xpos = self._get_fk(qpos)
                        target_np = xpos.detach().cpu().numpy()
                        if np.linalg.norm(fk_xpos - target_np) <= 1e-4:

embodichain/lab/sim/solvers/srs_solver.py:778

  • When no IK solution is found, this CPU get_ik returns qpos with shape (num_targets, 7), but the success path for return_all_solutions=False returns (num_targets, 1, 7) (via _process_single_solution). Several call sites index ik_qpos[:, 0, :] unconditionally, so the failure return should also be 3D (e.g., zeros with shape (num_targets, 1, 7)).

This issue also appears on line 1302 of the same file.

                            all_solutions[target_idx, sol_idx, :] = qpos
                            sol_idx += 1
            solution_counts[target_idx] = sol_idx

embodichain/lab/sim/solvers/srs_solver.py:1306

  • CUDA get_ik returns qpos with shape (num_targets, 7) when no solution is found, but returns (num_targets, 1, 7) on success (via _process_single_solution). This inconsistent shape breaks code that unconditionally indexes the first solution (e.g., ik_qpos[:, 0, :]). Return a consistently-shaped tensor on failure (typically zeros with shape (num_targets, 1, 7) when return_all_solutions=False; and a 3D empty/zero tensor when return_all_solutions=True).
            return (
                torch.zeros(num_targets, dtype=torch.bool, device=self.device),
                torch.zeros(
                    (num_targets, 7),
                    dtype=torch.float32,

@yuecideng yuecideng left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Follow-up review with the three requested findings (items 1, 2, and 5).

Comment thread embodichain/utils/warp/kinematics/srs_solver.py Outdated
Comment thread embodichain/lab/sim/solvers/srs_solver.py Outdated
Comment thread embodichain/utils/warp/kinematics/srs_solver.py Outdated
@yuecideng

Copy link
Copy Markdown
Contributor

It would be better to add an example to demo this feature (may extend the existed SRS solver example)

Copilot AI review requested due to automatic review settings August 25, 2026 08:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread embodichain/lab/sim/solvers/srs_solver.py
Copilot AI review requested due to automatic review settings August 25, 2026 09:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

embodichain/lab/sim/solvers/srs_solver.py:202

  • _wrap_to_limits() uses np.rint() to choose the nearest 2π-shift, but NumPy rounds half-way cases to even (bankers rounding). The CUDA/Warp implementation uses floor(x + 0.5), so for exact half-way cases (e.g., (seed-value)/2π == 0.5) CPU and CUDA can pick different wraps, undermining the stated CPU/CUDA parity. Use the same rounding rule as Warp (floor(x + 0.5)) here.
            k_min = int(np.ceil((lower - value) / two_pi))
            k_max = int(np.floor((upper - value) / two_pi))
            if k_min > k_max:
                return None
            nearest_k = int(np.rint((seed[index] - value) / two_pi))
            nearest_k = min(max(nearest_k, k_min), k_max)
            wrapped[index] = value + nearest_k * two_pi

Copilot AI review requested due to automatic review settings August 25, 2026 12:22
@chase6305 chase6305 closed this Aug 25, 2026
Comment thread embodichain/lab/sim/solvers/srs_solver.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread embodichain/lab/sim/solvers/srs_solver.py Outdated
@chase6305 chase6305 reopened this Aug 25, 2026
Copilot AI review requested due to automatic review settings August 26, 2026 03:34
Comment thread embodichain/lab/sim/solvers/srs_solver.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

embodichain/lab/sim/solvers/srs_solver.py:925

  • In _temporary_array, the scratch-array cache key ignores dtype. If the same (count, name) is later requested with a different dtype (easy to do when refactoring), the solver will reuse an array of the wrong type, which can cause Warp kernel type mismatches or silent memory corruption.
    def _temporary_array(self, count: int, dtype: type, name: str) -> wp.array:
        """Return a zeroed reusable Warp scratch array."""
        key = (count, name)
        array = self._temporary_workspace.get(key)

scripts/tutorials/sim/srs_solver.py:185

  • The path-planning DP can crash when the first waypoint has no candidates within max_joint_step_deg: first_cost becomes all inf, then at the next waypoint reachable_previous is all-false and reachable_edges.abs().amax(...).min() errors on an empty tensor. Add an explicit check after building first_cost to fail with a clear message.
        first_allowed = first_delta.abs().amax(dim=1) <= max_joint_step
        first_cost = (first_delta.square() * continuity_weights).sum(dim=1)
        first_cost.masked_fill_(~first_allowed, float("inf"))
        path_costs.append(first_cost)
        predecessors.append(torch.full_like(first_cost, -1, dtype=torch.long))

Comment thread tests/sim/solvers/test_srs_solver.py
Copilot AI review requested due to automatic review settings August 27, 2026 11:34
@greptile-apps

greptile-apps Bot commented Aug 27, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Start a greploop in Codex and it will work through the open comments and keep going until this PR reviews clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 28, 2026 02:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread embodichain/lab/sim/solvers/srs_solver.py
Copilot AI review requested due to automatic review settings August 28, 2026 03:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Suppressed comments (1)

embodichain/lab/sim/solvers/srs_solver.py:1059

  • CUDA all-solution outputs are packed and then padded with zeros, which is ambiguous now that all-zero joint vectors can be valid solutions (the validity mask is no longer based on nonzero sums). Padding with NaNs (or returning a count/mask) would let callers reliably filter padding.
        valid_qpos_list = [
            self._deduplicate_solutions(
                ik_solutions_tensor[i][ik_valid_flags_tensor[i]]
            )
            for i in range(num_targets)
        ]
        max_solutions = max(q.shape[0] for q in valid_qpos_list)
        valid_qpos_tensor = torch.zeros(
            (num_targets, max_solutions, 7),
            dtype=torch.float32,
            device=self.device,
        )
        for i, q in enumerate(valid_qpos_list):
            valid_qpos_tensor[i, : q.shape[0]] = q.to(self.device)

        return success_flags.to(self.device), valid_qpos_tensor

Comment thread embodichain/lab/sim/solvers/srs_solver.py Outdated
Comment thread embodichain/lab/sim/solvers/srs_solver.py Outdated
Comment thread embodichain/lab/sim/solvers/srs_solver.py Outdated
Copilot AI review requested due to automatic review settings August 28, 2026 04:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

scripts/tutorials/sim/srs_solver.py:216

  • The failure path when no globally continuous IK path exists assumes the previous layer has at least one reachable candidate. If all candidates were already unreachable under max_joint_step_deg (e.g., first waypoint constraints are too strict), reachable_previous becomes all-False and edge_delta[:, reachable_previous].abs().amax(...).min() will error on an empty tensor, hiding the real planning failure.
            if not bool(torch.isfinite(best_cost).any()):
                reachable_previous = torch.isfinite(path_costs[-1])
                reachable_edges = edge_delta[:, reachable_previous]
                smallest_step = reachable_edges.abs().amax(dim=2).min()
                raise RuntimeError(

embodichain/lab/sim/solvers/srs_solver.py:913

  • _temporary_array() caches Warp scratch arrays by (count, name) only. If a future call reuses the same name/count with a different dtype (e.g., float vs wp.vec3), the cache will return an array of the wrong dtype, leading to incorrect kernel launches or runtime errors that are hard to diagnose.
        self._temporary_workspace: dict[tuple[int, str], wp.array] = {}

    def _temporary_array(self, count: int, dtype: type, name: str) -> wp.array:
        """Return a zeroed reusable Warp scratch array."""
        key = (count, name)
        array = self._temporary_workspace.get(key)

Copilot AI review requested due to automatic review settings August 28, 2026 04:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

embodichain/lab/sim/solvers/srs_solver.py:833

  • When no IK solutions exist, this branch returns solutions with shape (N, 1, 7) for return_all_solutions=False (and (N, 0, 7) for all-solutions). That makes SRSSolver’s single-solution output rank-3, which is inconsistent with the BaseSolver.get_ik() docstring (expects (num_envs, num_joints)) and other solver implementations (e.g. URSolver.get_ik() returns (N, DOF) when return_all_solutions=False). This inconsistency can break callers that use solver instances directly (outside Robot.compute_ik() which reshapes). Consider returning (N, 7) for the single-solution case (including the failure fallback), and reserving rank-3 only for return_all_solutions=True, or update the base solver contract + other solvers/docs/tests to match always-rank-3.
            return (
                torch.zeros(num_targets, dtype=torch.bool, device=self.device),
                torch.zeros(
                    (num_targets, 0 if return_all_solutions else 1, 7),
                    dtype=qpos_seed.dtype,
                    device=self.device,
                ),

@chase6305
chase6305 merged commit c2d3065 into main Aug 28, 2026
6 checks passed
@chase6305
chase6305 deleted the cjt/main/fix_srs_solver branch August 28, 2026 06:01
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.

3 participants