Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use 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
.claude/skills/dexforce-add-solver/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 146% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 157% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 126% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 398% | 0% |
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.
embodichain/lab/sim/motion/solvers/
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:
Cfg + Solver classes fromembodichain/lab/sim/motion/solvers/__init__.py.
docs/source/overview/sim/motion/solvers/index.rst.
Ask the user (only what is not already stated):
<name>_solver) and a one-line description.(Pytorch, Differential, Pink, Pinocchio), or neural (NeuralIK).
from (cite it in the docs, as the UR solver cites ur-analytic-ik).
File: embodichain/lab/sim/motion/solvers/<name>_solver.py
Required pieces (mirror ur_solver.py):
from __future__ import annotations after the header.@configclass config class <Name>SolverCfg(SolverCfg):__post_init__ that populates derived fields (e.g. per-variant DHparameters) and raises ValueError for unknown variants — fail fast.
init_solver(self, device=..., **kwargs) -> "<Name>Solver" thatconstructs the solver and calls solver.set_tcp(self._get_tcp_as_numpy()).
<Name>Solver(BaseSolver) class:__init__(self, cfg, device, **kwargs) calls super().__init__(...),sets self.dof, and initializes solver-specific state.
get_ik(self, target_xpos, qpos_seed, return_all_solutions=False, **kwargs)returning (success, ik_qpos) (or (validity, all_solutions) when return_all_solutions=True). Shapes follow the BaseSolver.get_ik contract.
get_fk, set_tcp, get_qpos_limits, etc. from BaseSolver —do not reimplement FK unless the solver has a custom chain.
dh_matrix) only when genuinely needed.__all__ = ["<Name>SolverCfg", "<Name>Solver"].Template:
python# ---------------------------------------------------------------------------- # 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"]
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/):
from __future__ import annotations.@wp.struct for solver parameters (e.g. URParam) and pass itto the kernel as an input.
dim=(n_sample,)).then convert back to torch with wp.to_torch(...).
standardize_device_string(self.device) fromembodichain.utils.device_utils to get the Warp device string.
For pure-PyTorch / numerical solvers, skip this step entirely and implement get_ik directly with torch ops.
__init__.pyAdd the import + keep __all__ (if present) consistent in embodichain/lab/sim/motion/solvers/__init__.py:
pythonfrom .foo_solver import FooSolverCfg, FooSolver
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 / whatapproach it uses, the GPU/numerical backend).
python code block constructing the Cfg and callingcfg.init_solver(device=...). Use a .. tip:: Sphinx directive for the one parameter that usually matters.
get_fk (inherited), get_ik (with fullsignature, parameters, returns, and a runnable Example code block showing both return_all_solutions=False and True), set_tcp, and any static helpers. Use Google-style param lists and + bullets as in ur_solver.md.
Then add the page to the toctree in docs/source/overview/sim/motion/solvers/index.rst:
rst.. toctree:: :maxdepth: 1 pytorch_solver.md ... foo_solver.md
File: tests/sim/motion/solvers/test_<name>_solver.py — follow test_ur_solver.py exactly:
from __future__ import annotations (after header).grid_sample_qpos_from_limits(...) helper (reuse the one fromtest_ur_solver.py) to sample joint configs within limits with a safety margin.
BaseSolverTest class with:setup_simulation(self, device) — builds a SimulationManagerCfg,a RobotCfg whose solver_cfg={"arm": <Name>SolverCfg(...)} uses the new solver, and adds the robot via self.sim.add_robot(cfg=cfg).
test_ik(self) — the round-trip contract:compute_batch_fk → fk_xpos (both matrix and xyzquat forms).compute_batch_ik on both pose forms; assert the two IK results match.sample_qpos ≈ ik_qpos andfk_xpos ≈ ik_xpos with atol=5e-3, rtol=5e-3.
res[0] == False and the output shape.teardown_method calling self.sim.destroy().setup_method:class TestFooSolverCUDA(BaseSolverTest): setup_method → "cuda"class TestFooSolver(BaseSolverTest): setup_method → "cpu"if __name__ == "__main__": block running pytest.main(["-v", "-s", __file__]).Extend scripts/benchmark/robotics/kinematic_solver/run_benchmark.py (do not create a separate benchmark file — the kinematic-solver benchmark is unified):
TCP, etc. (mirror UR_LOWER_LIMITS / UR_UPPER_LIMITS / UR_TCP).
SUPPORTED_SOLVERS, add its short name thereand update _normalize_selected_solvers / the --solvers argparse choices.
_init_<name>_solver(device) -> <Name>Solver and_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()).
benchmark_<name>_solver() -> (perf_rows, metric_rows) mirroringbenchmark_ur_solver: iterate SAMPLE_SIZES, run CPU (+ optional CUDA), verify accuracy via get_pose_err, and append rows for both the Time & Memory and Success & Other Metrics tables.
run_all_benchmarks() behind an if "<name>" in solvers_to_run:guard, extending perf_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).
bashconda 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):
bashpytest tests/sim/motion/solvers/test_<name>_solver.py::TestFooSolver -v
Smoke-run the benchmark for the new solver only:
bashpython -m scripts.benchmark.robotics.kinematic_solver.run_benchmark -s <name>
Finally, run the /pre-commit-check skill to catch all CI violations locally.
from __future__ import annotations after the header@configclass on the Cfg, inheriting SolverCfg__post_init__ raises ValueError on unknown variantsinit_solver constructs the solver and calls set_tcp(...)BaseSolver, sets self.dof, implements get_ik__all__ declared in the solver module.. tip::, .. attention::)embodichain/lab/sim/motion/solvers/__init__.pyindex.rst toctreeblack run on all changed files| 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 |
| 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 |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 38,896 | 50,737 | +30% | 1 | 1 | 0% | 8,287 | 4,765 | -43% | 0 | 0 | — |
case-02 | fail→fail | 41,382 | 17,935 | -57% | 1 | 1 | 0% | 8,270 | 4,759 | -42% | 0 | 0 | — |
case-03 | fail→fail | 41,995 | 7,414 | -82% | 1 | 1 | 0% | 8,275 | 4,713 | -43% | 0 | 0 | — |
case-04 | pass→pass | 16,937 | 13,761 | -19% | 1 | 1 | 0% | 2,649 | 6,559 | +148% | 0 | 0 | — |
case-05 | pass→pass | 20,631 | 19,946 | -3% | 1 | 1 | 0% | 3,340 | 7,532 | +126% | 0 | 0 | — |
case-06 | pass→pass | 18,061 | 35,242 | +95% | 1 | 1 | 0% | 2,916 | 10,034 | +244% | 0 | 0 | — |
case-07 | fail→pass | 15,446 | 7,753 | -50% | 1 | 1 | 0% | 2,524 | 5,480 | +117% | 0 | 0 | — |
case-08 | fail→pass | 16,341 | 7,673 | -53% | 1 | 1 | 0% | 2,184 | 5,371 | +146% | 0 | 0 | — |
case-09 | fail→fail | 16,537 | 5,982 | -64% | 1 | 1 | 0% | 2,362 | 5,231 | +121% | 0 | 0 | — |
case-10 | fail→pass | 13,621 | 6,462 | -53% | 1 | 1 | 0% | 2,097 | 5,385 | +157% | 0 | 0 | — |
case-11 | fail→pass | 14,432 | 8,072 | -44% | 1 | 1 | 0% | 2,554 | 5,783 | +126% | 0 | 0 | — |
case-12 | fail→pass | 6,045 | 2,712 | -55% | 1 | 1 | 0% | 920 | 4,580 | +398% | 0 | 0 | — |
case-13 | fail→fail | 19,769 | 16,291 | -18% | 1 | 1 | 0% | 3,216 | 7,478 | +133% | 0 | 0 | — |
case-14 | fail→pass | 35,314 | 10,338 | -71% | 1 | 1 | 0% | 1,525 | 6,110 | +301% | 0 | 0 | — |
case-15 | fail→pass | 10,471 | 6,540 | -38% | 1 | 1 | 0% | 1,578 | 5,204 | +230% | 0 | 0 | — |
case-16 | fail→pass | 17,598 | 7,734 | -56% | 1 | 1 | 0% | 2,762 | 5,678 | +106% | 0 | 0 | — |
case-17 | fail→fail | 18,219 | 4,688 | -74% | 1 | 1 | 0% | 3,230 | 4,932 | +53% | 0 | 0 | — |
case-18 | fail→pass | 18,053 | 8,900 | -51% | 1 | 1 | 0% | 3,047 | 5,706 | +87% | 0 | 0 | — |
case-19 | fail→pass | 12,988 | 9,320 | -28% | 1 | 1 | 0% | 2,194 | 5,438 | +148% | 0 | 0 | — |
case-20 | pass→pass | 12,177 | 8,483 | -30% | 1 | 1 | 0% | 2,018 | 5,819 | +188% | 0 | 0 | — |
case-21 | fail→pass | 14,701 | 5,119 | -65% | 1 | 1 | 0% | 2,216 | 5,088 | +130% | 0 | 0 | — |
case-22 | fail→pass | 16,454 | 5,683 | -65% | 1 | 1 | 0% | 2,611 | 5,005 | +92% | 0 | 0 | — |
case-23 | fail→pass | 21,354 | 10,580 | -50% | 1 | 1 | 0% | 3,769 | 6,034 | +60% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 19 counted toward the lift figure. The other 4 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +57 percentage points is the difference between those two pass rates over the 19 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/22/2026 | +55% |
Other measured skills in the registry, with their headline benchmark lift.