Add Solver
SkillDev toolsUse when adding a new kinematic (IK/FK) solver to EmbodiChain — implements the solver module, its Sphinx docs page, the unit test, and the benchmark entry together
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Add Solver skill
What this skill tells your AI
The instructions your AI receives, as published by dexforce/embodichain in .agents/skills/add-solver/SKILL.md and read by ahel’s review.
Scaffold a new kinematic solver — and its three required companion artifacts
(docs, unit test, benchmark) — following EmbodiChain's SolverCfg / BaseSolver
pattern. The reference implementation is the UR analytic solver; use it as the
gold standard for structure and style.
When to Use
- User asks to "add a solver", "add an IK solver", "add a kinematic solver"
- A new robot family needs a closed-form / numerical / Warp-kernel IK backend
- The request names a solver that does not yet exist under
embodichain/lab/sim/motion/solvers/
The Four Artifacts
A new solver is not complete until all four exist and pass. Each must follow the conventions below.
| # | Artifact | Path |
|---|---|---|
| 1 | Solver module | embodichain/lab/sim/motion/solvers/<name>_solver.py |
| 2 | (GPU/Warp only) Warp kernel | embodichain/utils/warp/kinematics/<name>_solver.py |
| 3 | Sphinx docs page | docs/source/overview/sim/motion/solvers/<name>_solver.md |
| 4 | Unit test | tests/sim/motion/solvers/test_<name>_solver.py |
| 5 | Benchmark entry | extend scripts/benchmark/robotics/kinematic_solver/run_benchmark.py |
Plus two registration edits:
- Export the new
Cfg+Solverclasses fromembodichain/lab/sim/motion/solvers/__init__.py. - Add the docs page to the toctree in
docs/source/overview/sim/motion/solvers/index.rst.
Steps
1. Gather Solver Requirements
Ask the user (only what is not already stated):
- Solver name (
<name>_solver) and a one-line description. - Robot family / kinematic type — which robot(s) it targets, DOF.
- Approach — analytical closed-form (UR, OPW, SRS), numerical/Jacobian (Pytorch, Differential, Pink, Pinocchio), or neural (NeuralIK).
- Backend — pure PyTorch, NVIDIA Warp GPU kernel, or both.
- URDF / DH / OPW parameter source — link or file the parameters come
from (cite it in the docs, as the UR solver cites
ur-analytic-ik).
2. Write the Solver Module
File: embodichain/lab/sim/motion/solvers/<name>_solver.py
Required pieces (mirror ur_solver.py):
- Apache 2.0 copyright header (the 15-line block).
from __future__ import annotationsafter the header.- A
@configclassconfig class<Name>SolverCfg(SolverCfg):- Fields for any robot-specific parameters (DH params, OPW params, etc.).
- A
__post_init__that populates derived fields (e.g. per-variant DH parameters) and raisesValueErrorfor unknown variants — fail fast. - An
init_solver(self, device=..., **kwargs) -> "<Name>Solver"that constructs the solver and callssolver.set_tcp(self._get_tcp_as_numpy()).
- A
<Name>Solver(BaseSolver)class:__init__(self, cfg, device, **kwargs)callssuper().__init__(...), setsself.dof, and initializes solver-specific state.- Implements
get_ik(self, target_xpos, qpos_seed, return_all_solutions=False, **kwargs)returning(success, ik_qpos)(or(validity, all_solutions)whenreturn_all_solutions=True). Shapes follow theBaseSolver.get_ikcontract. - Reuses
get_fk,set_tcp,get_qpos_limits, etc. fromBaseSolver— do not reimplement FK unless the solver has a custom chain. - Add static helpers (e.g.
dh_matrix) only when genuinely needed.
__all__ = ["<Name>SolverCfg", "<Name>Solver"].
Template:
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
# ... (full Apache 2.0 header) ...
# ----------------------------------------------------------------------------
from __future__ import annotations
import torch
import numpy as np
from embodichain.utils import configclass
from embodichain.lab.sim.motion.solvers import SolverCfg, BaseSolver
from embodichain.data import get_data_path
@configclass
class FooSolverCfg(SolverCfg):
# robot-specific parameters, with sensible defaults
robot_type: str = "foo"
urdf_path: str = get_data_path("Foo/foo.urdf")
def __post_init__(self):
super().__post_init__()
if self.robot_type == "foo":
...
else:
raise ValueError(f"Unknown robot type: {self.robot_type}")
def init_solver(self, device: torch.device = torch.device("cpu"), **kwargs) -> "FooSolver":
"""Initialize the solver with the configuration.
Args:
device: The device to use for the solver. Defaults to CPU.
**kwargs: Additional keyword arguments for solver initialization.
Returns:
FooSolver: An initialized solver instance.
"""
solver = FooSolver(cfg=self, device=device, **kwargs)
solver.set_tcp(self._get_tcp_as_numpy())
return solver
class FooSolver(BaseSolver):
def __init__(self, cfg: FooSolverCfg, device: str, **kwargs):
super().__init__(cfg, device, **kwargs)
self.dof = 6
# init solver-specific state / Warp params here
def get_ik(self, target_xpos, qpos_seed, return_all_solutions: bool = False, **kwargs):
"""Compute target joint positions.
Args:
target_xpos (torch.Tensor): Target end-effector pose, shape (n_sample, 4, 4).
qpos_seed (torch.Tensor): Reference joint positions, shape (n_sample, num_joints).
return_all_solutions (bool): Return all candidates instead of the closest. Defaults to False.
**kwargs: Additional arguments for future extensions.
Returns:
Tuple[torch.Tensor, torch.Tensor]: (success, target_joints).
"""
...
return ik_validity, ik_qpos
__all__ = ["FooSolverCfg", "FooSolver"]
3. (GPU/Warp only) Write the Warp Kernel
For analytic solvers evaluated in batch on the GPU (UR, OPW, SRS), put the
@wp.kernel / @wp.func / @wp.struct definitions in
embodichain/utils/warp/kinematics/<name>_solver.py — not in the solver
module. The solver module imports the kernel and any param struct from there
and launches it with wp.launch.
Conventions (see ur_solver.py under utils/warp/kinematics/):
- Apache header +
from __future__ import annotations. - Define a
@wp.structfor solver parameters (e.g.URParam) and pass it to the kernel as an input. - One kernel thread per target pose (
dim=(n_sample,)). - Write candidate solutions and validity flags into preallocated Warp arrays,
then convert back to torch with
wp.to_torch(...). - Use
standardize_device_string(self.device)fromembodichain.utils.device_utilsto get the Warp device string.
For pure-PyTorch / numerical solvers, skip this step entirely and implement
get_ik directly with torch ops.
4. Register in __init__.py
Add the import + keep __all__ (if present) consistent in
embodichain/lab/sim/motion/solvers/__init__.py:
from .foo_solver import FooSolverCfg, FooSolver
5. Write the Docs Page
File: docs/source/overview/sim/motion/solvers/<name>_solver.md — mirror the structure
of ur_solver.md:
# <Name>Solver— one-paragraph intro (what it solves, why it's fast / what approach it uses, the GPU/numerical backend).- Cite the reference implementation / parameter source with a link.
- Key Features — bullet list.
- Kinematic model / parameters (DH table, OPW params, etc.) as needed.
- Configuration — a
pythoncode block constructing theCfgand callingcfg.init_solver(device=...). Use a.. tip::Sphinx directive for the one parameter that usually matters. - Main Methods — document
get_fk(inherited),get_ik(with full signature, parameters, returns, and a runnable Example code block showing bothreturn_all_solutions=FalseandTrue),set_tcp, and any static helpers. Use Google-style param lists and+bullets as inur_solver.md. - How It Works — numbered explanation of the solve pipeline.
- References — markdown links.
Then add the page to the toctree in docs/source/overview/sim/motion/solvers/index.rst:
.. toctree::
:maxdepth: 1
pytorch_solver.md
...
foo_solver.md
6. Write the Unit Test
File: tests/sim/motion/solvers/test_<name>_solver.py — follow test_ur_solver.py
exactly:
- Apache header +
from __future__ import annotations(after header). - A
grid_sample_qpos_from_limits(...)helper (reuse the one fromtest_ur_solver.py) to sample joint configs within limits with a safety margin. - A
BaseSolverTestclass with:setup_simulation(self, device)— builds aSimulationManagerCfg, aRobotCfgwhosesolver_cfg={"arm": <Name>SolverCfg(...)}uses the new solver, and adds the robot viaself.sim.add_robot(cfg=cfg).test_ik(self)— the round-trip contract:- Sample qpos from the robot's joint limits.
compute_batch_fk→fk_xpos(both matrix and xyzquat forms).compute_batch_ikon both pose forms; assert the two IK results match.- Re-run FK on the IK output; assert
sample_qpos ≈ ik_qposandfk_xpos ≈ ik_xposwithatol=5e-3, rtol=5e-3. - Feed an unreachable pose; assert
res[0] == Falseand the output shape.
teardown_methodcallingself.sim.destroy().
- Two concrete subclasses driving
setup_method:class TestFooSolverCUDA(BaseSolverTest): setup_method → "cuda"class TestFooSolver(BaseSolverTest): setup_method → "cpu"
if __name__ == "__main__":block runningpytest.main(["-v", "-s", __file__]).
7. Add the Benchmark Entry
Extend scripts/benchmark/robotics/kinematic_solver/run_benchmark.py (do not
create a separate benchmark file — the kinematic-solver benchmark is unified):
- Add module-level constants for the new solver's joint limits, joint names,
TCP, etc. (mirror
UR_LOWER_LIMITS/UR_UPPER_LIMITS/UR_TCP). - If the solver is not yet in
SUPPORTED_SOLVERS, add its short name there and update_normalize_selected_solvers/ the--solversargparse choices. - Write
_init_<name>_solver(device) -> <Name>Solverand_timed_<name>_ik_call(solver, fk_xpos, qpos_seed)helpers, mirroring_init_ur_solver/_timed_ur_ik_call(3-iteration timing skipping the first run,_sync_cuda(),_reset_peak_gpu_memory(),_memory_snapshot()). - Write
benchmark_<name>_solver() -> (perf_rows, metric_rows)mirroringbenchmark_ur_solver: iterateSAMPLE_SIZES, run CPU (+ optional CUDA), verify accuracy viaget_pose_err, and append rows for both theTime & MemoryandSuccess & Other Metricstables. - Wire it into
run_all_benchmarks()behind anif "<name>" in solvers_to_run:guard, extendingperf_rows/metric_rows. The leaderboard (_build_leaderboard_rows) and the markdown report (_write_markdown_report) are shared — the report must contain exactly the three tables (Time & Memory,Success & Other Metrics,Leaderboard).
For the full benchmark conventions (timing, memory, three-table report), defer
to the benchmark skill (.agents/skills/benchmark/SKILL.md).
8. Format and Verify
conda activate embodichain
black embodichain/lab/sim/motion/solvers/<name>_solver.py
black embodichain/utils/warp/kinematics/<name>_solver.py # if added
black tests/sim/motion/solvers/test_<name>_solver.py
black scripts/benchmark/robotics/kinematic_solver/run_benchmark.py
Run the unit test (CPU class is enough for a quick check):
pytest tests/sim/motion/solvers/test_<name>_solver.py::TestFooSolver -v
Smoke-run the benchmark for the new solver only:
python -m scripts.benchmark.robotics.kinematic_solver.run_benchmark -s <name>
Finally, run the /pre-commit-check skill to catch all CI violations locally.
Code Style Checklist
- Apache 2.0 header on every new file (solver, warp kernel, test)
-
from __future__ import annotationsafter the header -
@configclasson the Cfg, inheritingSolverCfg -
__post_init__raisesValueErroron unknown variants -
init_solverconstructs the solver and callsset_tcp(...) - Solver inherits
BaseSolver, setsself.dof, implementsget_ik -
__all__declared in the solver module - Google-style docstrings with Sphinx directives (
.. tip::,.. attention::) - Cfg + Solver exported from
embodichain/lab/sim/motion/solvers/__init__.py - Docs page added to
index.rsttoctree - Test covers FK↔IK round-trip + unreachable-pose failure, CPU & CUDA classes
- Benchmark entry added with CPU (+CUDA) timing, accuracy, and the 3-table report
-
blackrun on all changed files
Common Mistakes
| Mistake | Fix |
|---|---|
| Reimplementing FK / TCP / joint limits in the solver | Reuse BaseSolver.get_fk, set_tcp, set_qpos_limits |
| Putting the Warp kernel inside the solver module | Put @wp.kernel/@wp.struct in embodichain/utils/warp/kinematics/<name>_solver.py and import it |
Not exporting the Cfg/Solver from __init__.py | Add the import line so from embodichain.lab.sim.motion.solvers import FooSolverCfg works |
| Forgetting the docs toctree entry | Add the .md to docs/source/overview/sim/motion/solvers/index.rst |
| Test only checks happy-path IK | Must verify FK↔IK round-trip equality AND an unreachable-pose returns False |
| Creating a separate benchmark file | Extend the unified run_benchmark.py instead |
Skipping black / pre-commit | CI checks every file including tests and benchmarks |
Missing __post_init__ validation | Unknown robot variants must raise ValueError at config time |
Quick Reference
| Action | Command / Path |
|---|---|
| Reference solver | embodichain/lab/sim/motion/solvers/ur_solver.py |
| Reference Warp kernel | embodichain/utils/warp/kinematics/ur_solver.py |
| Reference docs | docs/source/overview/sim/motion/solvers/ur_solver.md |
| Reference test | tests/sim/motion/solvers/test_ur_solver.py |
| Benchmark file | scripts/benchmark/robotics/kinematic_solver/run_benchmark.py |
| Python env | conda activate embodichain |
| Run test (CPU) | pytest tests/sim/motion/solvers/test_<name>_solver.py::TestFooSolver -v |
| Run benchmark | python -m scripts.benchmark.robotics.kinematic_solver.run_benchmark -s <name> |
| Format | black <changed files> |
| Pre-commit | /pre-commit-check |
Signals
- GitHub stars
- 224
- Forks
- 24
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
add-solver- Source
- github.com/dexforce/embodichain