ssik 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. ssik-1.0.0/.gitignore +58 -0
  2. ssik-1.0.0/LICENSE +29 -0
  3. ssik-1.0.0/PKG-INFO +249 -0
  4. ssik-1.0.0/README.md +223 -0
  5. ssik-1.0.0/hatch_build.py +111 -0
  6. ssik-1.0.0/pyproject.toml +263 -0
  7. ssik-1.0.0/src/ssik/__init__.py +58 -0
  8. ssik-1.0.0/src/ssik/_kinbody.py +339 -0
  9. ssik-1.0.0/src/ssik/_pencil.py +357 -0
  10. ssik-1.0.0/src/ssik/_urdf.py +312 -0
  11. ssik-1.0.0/src/ssik/_version.py +24 -0
  12. ssik-1.0.0/src/ssik/cli.py +665 -0
  13. ssik-1.0.0/src/ssik/codegen/__init__.py +17 -0
  14. ssik-1.0.0/src/ssik/codegen/_compose/__init__.py +18 -0
  15. ssik-1.0.0/src/ssik/codegen/_compose/_target.py +44 -0
  16. ssik-1.0.0/src/ssik/codegen/_compose/general_6r.py +288 -0
  17. ssik-1.0.0/src/ssik/codegen/_compose/seven_r.py +245 -0
  18. ssik-1.0.0/src/ssik/codegen/_compose/spherical.py +192 -0
  19. ssik-1.0.0/src/ssik/codegen/_compose/spherical_two_intersecting.py +206 -0
  20. ssik-1.0.0/src/ssik/codegen/_compose/spherical_two_parallel.py +400 -0
  21. ssik-1.0.0/src/ssik/codegen/_compose/three_parallel.py +272 -0
  22. ssik-1.0.0/src/ssik/codegen/_symbolic/__init__.py +20 -0
  23. ssik-1.0.0/src/ssik/codegen/_symbolic/sp1.py +43 -0
  24. ssik-1.0.0/src/ssik/codegen/_symbolic/sp2.py +80 -0
  25. ssik-1.0.0/src/ssik/codegen/_symbolic/sp3.py +37 -0
  26. ssik-1.0.0/src/ssik/codegen/_symbolic/sp4.py +75 -0
  27. ssik-1.0.0/src/ssik/codegen/_symbolic/sp6.py +82 -0
  28. ssik-1.0.0/src/ssik/core/__init__.py +5 -0
  29. ssik-1.0.0/src/ssik/core/codegen.py +1209 -0
  30. ssik-1.0.0/src/ssik/core/dispatcher.py +333 -0
  31. ssik-1.0.0/src/ssik/core/solution.py +50 -0
  32. ssik-1.0.0/src/ssik/core/tolerances.py +78 -0
  33. ssik-1.0.0/src/ssik/core/topology.py +71 -0
  34. ssik-1.0.0/src/ssik/internals/__init__.py +41 -0
  35. ssik-1.0.0/src/ssik/kinematics/__init__.py +24 -0
  36. ssik-1.0.0/src/ssik/kinematics/_scalar3.py +159 -0
  37. ssik-1.0.0/src/ssik/kinematics/poe_fk.py +258 -0
  38. ssik-1.0.0/src/ssik/kinematics/poe_to_dh.py +334 -0
  39. ssik-1.0.0/src/ssik/kinematics/predicates.py +447 -0
  40. ssik-1.0.0/src/ssik/kinematics/reverse.py +139 -0
  41. ssik-1.0.0/src/ssik/manipulator.py +326 -0
  42. ssik-1.0.0/src/ssik/postprocess.py +208 -0
  43. ssik-1.0.0/src/ssik/prebuilt/README.md +76 -0
  44. ssik-1.0.0/src/ssik/prebuilt/__init__.py +0 -0
  45. ssik-1.0.0/src/ssik/prebuilt/franka_panda_ik.py +534 -0
  46. ssik-1.0.0/src/ssik/prebuilt/gen3_ik.py +201 -0
  47. ssik-1.0.0/src/ssik/prebuilt/iiwa14_ik.py +201 -0
  48. ssik-1.0.0/src/ssik/prebuilt/jaco2_ik.py +980 -0
  49. ssik-1.0.0/src/ssik/prebuilt/kassow_kr810_ik.py +3421 -0
  50. ssik-1.0.0/src/ssik/prebuilt/puma560_ik.py +634 -0
  51. ssik-1.0.0/src/ssik/prebuilt/rizon4_ik.py +4221 -0
  52. ssik-1.0.0/src/ssik/prebuilt/ur5_ik.py +594 -0
  53. ssik-1.0.0/src/ssik/py.typed +0 -0
  54. ssik-1.0.0/src/ssik/refinement/__init__.py +894 -0
  55. ssik-1.0.0/src/ssik/solvers/__init__.py +62 -0
  56. ssik-1.0.0/src/ssik/solvers/husty_pfurner/__init__.py +14 -0
  57. ssik-1.0.0/src/ssik/solvers/husty_pfurner/_back_substitute.py +498 -0
  58. ssik-1.0.0/src/ssik/solvers/husty_pfurner/_constraints.py +1180 -0
  59. ssik-1.0.0/src/ssik/solvers/husty_pfurner/_eliminate.py +1186 -0
  60. ssik-1.0.0/src/ssik/solvers/husty_pfurner/_study.py +278 -0
  61. ssik-1.0.0/src/ssik/solvers/husty_pfurner/general_6r.py +319 -0
  62. ssik-1.0.0/src/ssik/solvers/ikgeo/__init__.py +10 -0
  63. ssik-1.0.0/src/ssik/solvers/ikgeo/_raghavan_roth.py +1781 -0
  64. ssik-1.0.0/src/ssik/solvers/ikgeo/_univariate.py +249 -0
  65. ssik-1.0.0/src/ssik/solvers/ikgeo/general_6r.py +138 -0
  66. ssik-1.0.0/src/ssik/solvers/ikgeo/spherical.py +204 -0
  67. ssik-1.0.0/src/ssik/solvers/ikgeo/spherical_two_intersecting.py +199 -0
  68. ssik-1.0.0/src/ssik/solvers/ikgeo/spherical_two_parallel.py +224 -0
  69. ssik-1.0.0/src/ssik/solvers/ikgeo/three_parallel.py +190 -0
  70. ssik-1.0.0/src/ssik/solvers/ikgeo/two_intersecting.py +168 -0
  71. ssik-1.0.0/src/ssik/solvers/ikgeo/two_parallel.py +157 -0
  72. ssik-1.0.0/src/ssik/solvers/jointlock/__init__.py +13 -0
  73. ssik-1.0.0/src/ssik/solvers/jointlock/seven_r.py +667 -0
  74. ssik-1.0.0/src/ssik/solvers/numerical/__init__.py +23 -0
  75. ssik-1.0.0/src/ssik/solvers/numerical/lm_multi_restart.py +182 -0
  76. ssik-1.0.0/src/ssik/solvers/seven_r/__init__.py +12 -0
  77. ssik-1.0.0/src/ssik/solvers/seven_r/srs.py +479 -0
  78. ssik-1.0.0/src/ssik/solvers/seven_r/srs_polished.py +224 -0
  79. ssik-1.0.0/src/ssik/subproblems/__init__.py +21 -0
  80. ssik-1.0.0/src/ssik/subproblems/_aux.py +336 -0
  81. ssik-1.0.0/src/ssik/subproblems/_rotation.py +83 -0
  82. ssik-1.0.0/src/ssik/subproblems/_validate.py +33 -0
  83. ssik-1.0.0/src/ssik/subproblems/sp1.py +65 -0
  84. ssik-1.0.0/src/ssik/subproblems/sp2.py +115 -0
  85. ssik-1.0.0/src/ssik/subproblems/sp3.py +51 -0
  86. ssik-1.0.0/src/ssik/subproblems/sp4.py +104 -0
  87. ssik-1.0.0/src/ssik/subproblems/sp5.py +412 -0
  88. ssik-1.0.0/src/ssik/subproblems/sp6.py +299 -0
ssik-1.0.0/.gitignore ADDED
@@ -0,0 +1,58 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ *.egg
7
+ .eggs/
8
+ build/
9
+ dist/
10
+ wheels/
11
+ .venv/
12
+ venv/
13
+ env/
14
+
15
+ # Generated by hatch-vcs
16
+ src/ssik/_version.py
17
+
18
+ # Build artefacts
19
+ *.so
20
+ *.dylib
21
+ *.dll
22
+ *.o
23
+ *.obj
24
+ _skbuild/
25
+ CMakeCache.txt
26
+ CMakeFiles/
27
+ cmake_install.cmake
28
+ Makefile
29
+
30
+ # Generated solvers (development; the package will use ~/.cache/ssik/ at runtime)
31
+ generated/
32
+
33
+ # Tests
34
+ .pytest_cache/
35
+ .coverage
36
+ htmlcov/
37
+ .tox/
38
+
39
+ # Type checking / lint caches
40
+ .mypy_cache/
41
+ .ruff_cache/
42
+
43
+ # IDE / editor
44
+ .vscode/
45
+ .idea/
46
+ *.swp
47
+ .DS_Store
48
+
49
+ # mkdocs build output
50
+ site/
51
+ *.cpython-*-darwin.so
52
+ *.cpython-*-linux-gnu.so
53
+ src/ssik/**/*.c
54
+ src/ssik/**/*.html
55
+ # Cython artefacts from per-arm artifact compile (#137 Slice 3).
56
+ tests/artifacts/*.c
57
+ tests/artifacts/*.html
58
+ build/
ssik-1.0.0/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Siddhartha Srinivasa
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ssik-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.4
2
+ Name: ssik
3
+ Version: 1.0.0
4
+ Summary: Analytical inverse kinematics for 6R and 7R revolute arms. Returns every IK branch at machine precision.
5
+ Project-URL: Repository, https://github.com/personalrobotics/ssik
6
+ Project-URL: Issues, https://github.com/personalrobotics/ssik/issues
7
+ Author: Siddhartha Srinivasa
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Keywords: ik-geo,inverse-kinematics,kinematics,robotics
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: cython>=3.1
20
+ Requires-Dist: numpy>=2.0
21
+ Requires-Dist: scipy>=1.13
22
+ Requires-Dist: sympy>=1.10
23
+ Provides-Extra: urdf
24
+ Requires-Dist: urchin>=0.0.27; extra == 'urdf'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # ssik
28
+
29
+ [![PyPI](https://img.shields.io/pypi/v/ssik.svg)](https://pypi.org/project/ssik/)
30
+ [![Python](https://img.shields.io/pypi/pyversions/ssik.svg)](https://pypi.org/project/ssik/)
31
+ [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
32
+
33
+ Analytical inverse kinematics for 6R and 7R revolute robot arms. Each arm becomes a single self-contained Python module — `import franka_panda_ik; franka_panda_ik.solve(T)` — that returns **every IK branch** at machine-precision FK closure.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install ssik
39
+ ```
40
+
41
+ Python 3.11+. Wheels for Linux x86_64, macOS arm64, macOS x86_64, Windows x86_64.
42
+
43
+ ## Quickstart
44
+
45
+ ```python
46
+ from ssik.prebuilt import franka_panda_ik
47
+ import numpy as np
48
+
49
+ T_target = np.eye(4); T_target[:3, 3] = [0.5, 0.1, 0.3]
50
+ sols = franka_panda_ik.solve(T_target) # every analytical IK branch
51
+ ```
52
+
53
+ `sols` is a `list[Solution]`. Each `Solution` carries `q` (the joint vector), `fk_residual` (‖FK(q) − T‖), and which polish path fired. Empty list = pose is unreachable.
54
+
55
+ ## The artifact model
56
+
57
+ ssik is built around **per-arm artifact modules**. Each artifact is a single `.py` file with the per-arm KinBody constants, the dispatched solver, and any cached symbolic preprocessing already baked in. **No URDF parsing, no `urchin`, no `sympy` on the runtime import path.** A robot stack that imports `<arm>_ik.py` carries no algorithmic complexity beyond what the build pipeline already resolved.
58
+
59
+ This is the same idea OpenRAVE's IKFast had — generate per-arm specialised IK code at design time, run pure numeric at deployment — but without IKFast's brittleness on non-Pieper geometries.
60
+
61
+ There are two artifact paths:
62
+
63
+ ### Use a prebuilt arm (`ssik.prebuilt`)
64
+
65
+ The wheel ships 8 ready-to-import artifacts:
66
+
67
+ | Module | Arm | Class |
68
+ |---|---|---|
69
+ | `ur5_ik` | Universal Robots UR5 | three-parallel 6R |
70
+ | `puma560_ik` | KUKA Puma 560 | Pieper 6R (spherical wrist) |
71
+ | `jaco2_ik` | Kinova JACO 2 | **non-Pieper 6R** |
72
+ | `iiwa14_ik` | KUKA iiwa LBR 14 | SRS 7R |
73
+ | `gen3_ik` | Kinova Gen3 7-DOF | **approximate-SRS 7R** |
74
+ | `franka_panda_ik` | Franka Panda | anthropomorphic 7R |
75
+ | `rizon4_ik` | Flexiv Rizon 4 | **non-SRS 7R** |
76
+ | `kassow_kr810_ik` | Kassow KR810 | **non-SRS 7R** |
77
+
78
+ ```python
79
+ from ssik.prebuilt import iiwa14_ik
80
+ sols = iiwa14_ik.solve(T_target)
81
+ ```
82
+
83
+ For trajectory tracking and IK-based teleop, the canonical pattern is "give me the IK closest to where the robot is now":
84
+
85
+ ```python
86
+ # Robot's current configuration (from joint sensors, last command, etc.).
87
+ q_current = np.array([0.0, -0.5, 0.0, 0.7, 0.0, 1.2, 0.0])
88
+
89
+ # Target pose updates every control tick (VR controller, planner, etc.).
90
+ T_target = ...
91
+
92
+ # max_solutions=1 + q_seed: solver visits lock-samples in nearest-to-seed
93
+ # order and short-circuits on the first in-limits branch (~5-6× faster
94
+ # than the full sweep on 7R jointlock arms; sub-ms on 6R / SRS arms).
95
+ sols = franka_panda_ik.solve(T_target, max_solutions=1, q_seed=q_current)
96
+ q_command = sols[0].q if sols else q_current
97
+ ```
98
+
99
+ ### Build an artifact for your own arm
100
+
101
+ For any arm not in the prebuilt set, run `ssik build` once against the URDF:
102
+
103
+ ```bash
104
+ ssik build my_arm.urdf --base base_link --ee tool0
105
+ # → my_arm_ik.py
106
+ ```
107
+
108
+ Build time depends on solver class:
109
+ - **<1 s** for tier-0 closed-form (UR-class, Pieper, SRS-class 7R)
110
+ - **~30 s** for non-Pieper 6R (Raghavan–Roth symbolic derivation)
111
+ - **7–20 min** for non-SRS 7R (cached Husty–Pfurner per lock sample)
112
+
113
+ Ship the emitted `.py` alongside your robot stack. Once built, use it exactly like a prebuilt:
114
+
115
+ ```python
116
+ import my_arm_ik
117
+ sols = my_arm_ik.solve(T_target)
118
+ ```
119
+
120
+ Re-run `ssik build` after `pip install -U ssik` if you want the latest solver fixes. Old artifacts keep working — they're frozen against the ssik version that built them. `ssik build` requires the URDF extras: `pip install ssik[urdf]`.
121
+
122
+ ### Development path: `Manipulator.from_urdf` (not for deployment)
123
+
124
+ For one-off experiments before committing to a build artifact, ssik also exposes the runtime classifier as a Python class:
125
+
126
+ ```python
127
+ import ssik
128
+ arm = ssik.Manipulator.from_urdf("my_arm.urdf", base="base_link", ee="tool0")
129
+ sols = arm.solve(T_target, max_solutions=1, q_seed=q_current)
130
+ ```
131
+
132
+ Every fresh process re-runs URDF parsing, topology classification, and (for non-Pieper sub-chains) first-call sympy preprocessing — so this path is **strictly slower than the build-artifact path in production** and requires `urchin` + `sympy` on the runtime path (`pip install ssik[urdf]`). Once dispatch is settled, switch to `ssik build`.
133
+
134
+ Contributors extending ssik's own test fixtures (vs deploying for their own arm) use `ssik add-arm`; see [CONTRIBUTING.md](CONTRIBUTING.md#adding-a-new-arm-fixture).
135
+
136
+ ## What `solve()` returns
137
+
138
+ A `list[Solution]`. Each `Solution` has:
139
+
140
+ - `q` — joint-angle vector (length DOF)
141
+ - `fk_residual` — `‖FK(q) − T‖_F` (Frobenius norm against the original URDF / spec FK)
142
+ - `refinement_used` — `"none"` or `"lm"` if Levenberg–Marquardt polish fired
143
+
144
+ A single 6-DOF target pose admits up to **16 analytical IK branches** (8 typical for a Pieper-class arm: 4 shoulder × 2 elbow, with the wrist deterministic). For 7R redundant arms the IK is a 1-parameter family; ssik discretises it into 32–256 branches per pose depending on the swivel-sample count.
145
+
146
+ By default `solve()` runs **`respect_limits=True`**: out-of-URDF-limit branches are dropped (with a `q ± 2π` rescue pass first). On 7R jointlock arms the limits filter runs *during* the lock-sweep so `max_solutions=1` short-circuits on the first in-limits candidate rather than wasting samples on branches the postprocess would discard. Pass `respect_limits=False` for the raw geometric set.
147
+
148
+ The `allow_refinement=True` opt-in runs LM polish per algebraic candidate at a few hundred microseconds per branch — useful when an algebraic candidate lands just above `fk_atol` near a kinematic singularity.
149
+
150
+ ## Tuning knobs
151
+
152
+ ### `TolerancePolicy` — six thresholds, one object
153
+
154
+ `solve()` accepts an optional `policy=` kwarg. The default `ssik.DEFAULT_TOLERANCE_POLICY` works for every shipped fixture; reach for a custom policy when a real arm's URDF has structural near-degeneracies (axes that *almost* but not exactly meet) or when you want tighter / looser FK closure than the defaults provide.
155
+
156
+ ```python
157
+ from ssik import TolerancePolicy, DEFAULT_TOLERANCE_POLICY
158
+
159
+ policy = TolerancePolicy(
160
+ axis_parallel=1e-8, # ||a × b||: when two axes are "parallel"
161
+ axis_intersect=1e-8, # perpendicular distance: when two lines "meet"
162
+ subproblem_feasibility=1e-9,# is_ls boundary inside SP1-SP6
163
+ subproblem_numerical=1e-5, # FK-closure filter on algebraic candidates
164
+ subproblem_degeneracy=1e-12,# rank-drop threshold; below this, return []
165
+ subproblem_dedup=1e-3, # angle-space tolerance for collapsing duplicates
166
+ )
167
+ sols = my_arm_ik.solve(T_target, policy=policy)
168
+ ```
169
+
170
+ The fields are named for *why* they exist so log messages can say `"SP6 sign branch rejected: closure 1.2e-4 > subproblem_numerical 1e-5"` instead of citing magic numbers.
171
+
172
+ ### `ssik.postprocess` — composable filters
173
+
174
+ `solve()` returns the geometric IK set. For application-specific filtering, four helpers in `ssik.postprocess` compose into the typical "robot-aware IK" pipeline:
175
+
176
+ ```python
177
+ from ssik.postprocess import (
178
+ respect_limits, wrap_to_limits, nearest_to_seed, take_first,
179
+ )
180
+
181
+ sols = my_arm_ik.solve(T_target, respect_limits=False) # raw geometric set
182
+ sols = wrap_to_limits(sols, my_arm_ik._KB) # try q ± 2π to bring in
183
+ sols = respect_limits(sols, my_arm_ik._KB) # drop anything still outside
184
+ sols = nearest_to_seed(sols, q_current) # sort by wrap-to-pi distance
185
+ sols = take_first(sols, k=4) # top-k after ranking
186
+ ```
187
+
188
+ By default `solve()` already runs `wrap_to_limits` + `respect_limits`; the standalone helpers exist for callers who want a different order, a different metric, or to add their own filters (collision-aware filtering, dexterity scoring, custom seed metrics) between the layers.
189
+
190
+ Out of scope: collision filtering (use FCL or similar at the application layer) and continuous-trajectory smoothness (typically a separate planner concern).
191
+
192
+ ## How it compares
193
+
194
+ Numerical-IK libraries take a seed, run damped least-squares to a **single** converged configuration, and stop. ssik returns **every analytical branch** at machine precision. Branch enumeration matters for motion planning (try every branch, pick the one with best clearance), for dexterity analysis (the manipulability ellipsoid is per-branch), and for trajectory continuation across kinematic singularities.
195
+
196
+ EAIK (Ostermeier 2024) is the canonical Python wrapper around C++ subproblem-decomposition solvers. It's analytical on the kinematic families it recognises and refuses everything else. Numbers below from [`examples/04_compare_vs_eaik.py`](examples/04_compare_vs_eaik.py) over 100 random reachable poses per arm, Apple M3 single-thread, mean ± 95% CI via 1000-resample bootstrap. FK residual is the Frobenius norm `‖FK(q) − T‖` against the original URDF / spec FK.
197
+
198
+ | Arm (class) | EAIK | ssik |
199
+ |---|---|---|
200
+ | UR5 (Pieper 6R, three-parallel) | 5 ± 0 µs / FK 2e-15 / 4 sols | 556 ± 12 µs / FK 2e-9 / 4 sols |
201
+ | Puma 560 (Pieper 6R, spherical wrist) | 5 ± 1 µs / FK 3e-14 / 8 sols | 245 ± 3 µs / FK 2e-14 / 8 sols |
202
+ | JACO 2 (**non-Pieper 6R**) | **refuses** ("6R-Unknown Kinematic Class") | 1.02 ± 0.04 ms / FK 3e-6 / 2 sols |
203
+ | iiwa14 (SRS 7R) | **refuses** ("only 1-6R robots are solvable") | 5.10 ± 0.07 ms / FK 4e-13 / 96 sols |
204
+ | Gen3 (**approximate-SRS 7R**, 12 mm offset) | **refuses** ("only 1-6R") | 41.83 ± 1.15 ms / FK 1e-12 / 47 sols |
205
+ | Franka Panda (anthropomorphic 7R) | **refuses** ("only 1-6R") | 28.03 ± 2.57 ms / FK 7e-13 / 9 sols |
206
+ | Rizon 4 (**non-SRS 7R**) | **refuses** ("only 1-6R") | 30.88 ± 8.80 ms / FK 4e-9 / 35 sols |
207
+ | Kassow KR810 (**non-SRS 7R**) | **refuses** ("only 1-6R") | 28.06 ± 10.63 ms / FK 7e-8 / 24 sols |
208
+
209
+ EAIK is ~100× faster than ssik on Pieper-class 6R — that is its native sweet spot, and ssik does not try to compete there. The interesting cells are the **refuses** ones: non-Pieper 6R (JACO 2) and every 7R arm. Those are the geometries ssik exists for. The "refuses (...)" strings are EAIK's actual error messages, captured verbatim by the bench harness. A numerical-IK comparison (MINK) is tracked separately in [#236](https://github.com/personalrobotics/ssik/issues/236).
210
+
211
+ ## Under the hood
212
+
213
+ The algorithmic ingredients are not novel — Raghavan–Roth (1990), Manocha–Canny (1994), Singh–Kreutz (1989), Husty–Pfurner (2007). What's new is making the textbook pipelines survive on real ill-conditioned arms (AE-3 leftvar selection on JACO 2 drops `cond(m_quad)` from 3.75 × 10^16 to 127), composing them with a uniform dispatch layer, and packaging the whole thing as a deployable artifact.
214
+
215
+ Cython hot loops cover the leaf primitives (POE forward kinematics, the Levenberg–Marquardt polish and analytical Jacobian); the rest is pure Python so it stays inspectable.
216
+
217
+ **Bulletproof testing**: every solver lands with N-way cross-solver agreement on shared fixtures, FK closure ≤ 1e-10 on every retained IK, 500+ Hypothesis-fuzzed random poses per fixture, and an explicit speed bench that has to clear a regression gate. The current suite has **1300+ tests across 11 fixture arms**. Negative-result spikes (a Cython estimate that misses by 2-5×, a codegen-bake on a part that's 0.3% of runtime) are published as closed issues with profile data so the next contributor doesn't repeat the path.
218
+
219
+ ## Documentation
220
+
221
+ - [docs/arm_coverage.md](docs/arm_coverage.md) — per-arm fixture tables, tested speeds, source URDFs
222
+ - [docs/architecture.md](docs/architecture.md) — solver tier catalog, dispatch flow, build artifact internals, algorithmic lineage
223
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — repo layout, dev setup, testing discipline
224
+
225
+ ## Related libraries
226
+
227
+ ssik does not compete with these on the arms they cover. Pick the right tool for your geometry.
228
+
229
+ - [**EAIK**](https://github.com/OstermD/EAIK) (Ostermeier 2024) — Python wrapper around C++ subproblem-decomposition solvers. Analytical, returns all branches on Pieper-class 6R and canonical SRS 7R (with a manual joint lock). Refuses arms outside its recognised kinematic families. Directly benchmarked in the table above.
230
+ - [**IK-Geo**](https://github.com/rpiRobotics/ik-geo) (Elias–Wen 2022/2025) — the reference C++/Rust implementation of subproblem decomposition. Same coverage profile as EAIK. Has Python bindings (`ik-geo` on PyPI); currently pins `pyo3==0.20.3` so the wheel is incompatible with Python 3.13 — track upstream for an update.
231
+ - [**IKFast**](http://openrave.org/docs/latest_stable/openravepy/ikfast/) (Diankov 2010, part of OpenRAVE) — the original analytical-IK codegen tool. Symbolic preprocessing in sympy → per-arm C++. Works well on the kinematic families it was tuned for (Pieper-class 6R, spherical-wrist 7R via joint lock); the symbolic pipeline fails on modern sympy for non-Pieper geometries (`mpmath.polyroots` NoConvergence, `Matrix.inv` / `Matrix.det` stalls). LGPL-licensed.
232
+ - [**MINK**](https://github.com/kevinzakka/mink) (Zakka) — Mujoco-native numerical IK via damped least-squares. Iterative, takes a seed, converges to a single configuration. Handles any kinematic geometry but returns one IK, not all branches, and FK closure is proportional to the convergence tolerance (typically 1e-3 to 1e-6 rather than machine precision).
233
+ - [**TracIK**](https://traclabs.com/projects/trac-ik/) (Beeson & Ames 2015) — combined SQP / pseudoinverse Jacobian solver; the ROS Industrial default numerical IK. URDF-native. Same one-branch-per-seed semantics as MINK. The maintained Python binding (`pytracik`) ships a broken arm64 wheel; the ROS-native binding works fine inside ROS.
234
+ - [**KDL-LMA**](https://github.com/orocos/orocos_kinematics_dynamics) — OROCOS KDL's Levenberg-Marquardt numerical IK. Older and less robust than TracIK or MINK on the same problem class.
235
+
236
+ ## License
237
+
238
+ [BSD-3-Clause](LICENSE). The library incorporates clean-room reimplementations of algorithms from BSD-3-licensed IK-Geo (Elias–Wen 2022/2025) and from the academic publications of Raghavan–Roth (1990), Manocha–Canny (1994), Singh–Kreutz (1989), and Husty–Pfurner (2007). Algorithmic lineage is documented in module docstrings.
239
+
240
+ ## Citation
241
+
242
+ ```bibtex
243
+ @software{ssik,
244
+ author = {Srinivasa, Siddhartha},
245
+ title = {ssik: analytical inverse kinematics for 6R and 7R revolute arms},
246
+ url = {https://github.com/personalrobotics/ssik},
247
+ year = {2026},
248
+ }
249
+ ```
ssik-1.0.0/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # ssik
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/ssik.svg)](https://pypi.org/project/ssik/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/ssik.svg)](https://pypi.org/project/ssik/)
5
+ [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
6
+
7
+ Analytical inverse kinematics for 6R and 7R revolute robot arms. Each arm becomes a single self-contained Python module — `import franka_panda_ik; franka_panda_ik.solve(T)` — that returns **every IK branch** at machine-precision FK closure.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install ssik
13
+ ```
14
+
15
+ Python 3.11+. Wheels for Linux x86_64, macOS arm64, macOS x86_64, Windows x86_64.
16
+
17
+ ## Quickstart
18
+
19
+ ```python
20
+ from ssik.prebuilt import franka_panda_ik
21
+ import numpy as np
22
+
23
+ T_target = np.eye(4); T_target[:3, 3] = [0.5, 0.1, 0.3]
24
+ sols = franka_panda_ik.solve(T_target) # every analytical IK branch
25
+ ```
26
+
27
+ `sols` is a `list[Solution]`. Each `Solution` carries `q` (the joint vector), `fk_residual` (‖FK(q) − T‖), and which polish path fired. Empty list = pose is unreachable.
28
+
29
+ ## The artifact model
30
+
31
+ ssik is built around **per-arm artifact modules**. Each artifact is a single `.py` file with the per-arm KinBody constants, the dispatched solver, and any cached symbolic preprocessing already baked in. **No URDF parsing, no `urchin`, no `sympy` on the runtime import path.** A robot stack that imports `<arm>_ik.py` carries no algorithmic complexity beyond what the build pipeline already resolved.
32
+
33
+ This is the same idea OpenRAVE's IKFast had — generate per-arm specialised IK code at design time, run pure numeric at deployment — but without IKFast's brittleness on non-Pieper geometries.
34
+
35
+ There are two artifact paths:
36
+
37
+ ### Use a prebuilt arm (`ssik.prebuilt`)
38
+
39
+ The wheel ships 8 ready-to-import artifacts:
40
+
41
+ | Module | Arm | Class |
42
+ |---|---|---|
43
+ | `ur5_ik` | Universal Robots UR5 | three-parallel 6R |
44
+ | `puma560_ik` | KUKA Puma 560 | Pieper 6R (spherical wrist) |
45
+ | `jaco2_ik` | Kinova JACO 2 | **non-Pieper 6R** |
46
+ | `iiwa14_ik` | KUKA iiwa LBR 14 | SRS 7R |
47
+ | `gen3_ik` | Kinova Gen3 7-DOF | **approximate-SRS 7R** |
48
+ | `franka_panda_ik` | Franka Panda | anthropomorphic 7R |
49
+ | `rizon4_ik` | Flexiv Rizon 4 | **non-SRS 7R** |
50
+ | `kassow_kr810_ik` | Kassow KR810 | **non-SRS 7R** |
51
+
52
+ ```python
53
+ from ssik.prebuilt import iiwa14_ik
54
+ sols = iiwa14_ik.solve(T_target)
55
+ ```
56
+
57
+ For trajectory tracking and IK-based teleop, the canonical pattern is "give me the IK closest to where the robot is now":
58
+
59
+ ```python
60
+ # Robot's current configuration (from joint sensors, last command, etc.).
61
+ q_current = np.array([0.0, -0.5, 0.0, 0.7, 0.0, 1.2, 0.0])
62
+
63
+ # Target pose updates every control tick (VR controller, planner, etc.).
64
+ T_target = ...
65
+
66
+ # max_solutions=1 + q_seed: solver visits lock-samples in nearest-to-seed
67
+ # order and short-circuits on the first in-limits branch (~5-6× faster
68
+ # than the full sweep on 7R jointlock arms; sub-ms on 6R / SRS arms).
69
+ sols = franka_panda_ik.solve(T_target, max_solutions=1, q_seed=q_current)
70
+ q_command = sols[0].q if sols else q_current
71
+ ```
72
+
73
+ ### Build an artifact for your own arm
74
+
75
+ For any arm not in the prebuilt set, run `ssik build` once against the URDF:
76
+
77
+ ```bash
78
+ ssik build my_arm.urdf --base base_link --ee tool0
79
+ # → my_arm_ik.py
80
+ ```
81
+
82
+ Build time depends on solver class:
83
+ - **<1 s** for tier-0 closed-form (UR-class, Pieper, SRS-class 7R)
84
+ - **~30 s** for non-Pieper 6R (Raghavan–Roth symbolic derivation)
85
+ - **7–20 min** for non-SRS 7R (cached Husty–Pfurner per lock sample)
86
+
87
+ Ship the emitted `.py` alongside your robot stack. Once built, use it exactly like a prebuilt:
88
+
89
+ ```python
90
+ import my_arm_ik
91
+ sols = my_arm_ik.solve(T_target)
92
+ ```
93
+
94
+ Re-run `ssik build` after `pip install -U ssik` if you want the latest solver fixes. Old artifacts keep working — they're frozen against the ssik version that built them. `ssik build` requires the URDF extras: `pip install ssik[urdf]`.
95
+
96
+ ### Development path: `Manipulator.from_urdf` (not for deployment)
97
+
98
+ For one-off experiments before committing to a build artifact, ssik also exposes the runtime classifier as a Python class:
99
+
100
+ ```python
101
+ import ssik
102
+ arm = ssik.Manipulator.from_urdf("my_arm.urdf", base="base_link", ee="tool0")
103
+ sols = arm.solve(T_target, max_solutions=1, q_seed=q_current)
104
+ ```
105
+
106
+ Every fresh process re-runs URDF parsing, topology classification, and (for non-Pieper sub-chains) first-call sympy preprocessing — so this path is **strictly slower than the build-artifact path in production** and requires `urchin` + `sympy` on the runtime path (`pip install ssik[urdf]`). Once dispatch is settled, switch to `ssik build`.
107
+
108
+ Contributors extending ssik's own test fixtures (vs deploying for their own arm) use `ssik add-arm`; see [CONTRIBUTING.md](CONTRIBUTING.md#adding-a-new-arm-fixture).
109
+
110
+ ## What `solve()` returns
111
+
112
+ A `list[Solution]`. Each `Solution` has:
113
+
114
+ - `q` — joint-angle vector (length DOF)
115
+ - `fk_residual` — `‖FK(q) − T‖_F` (Frobenius norm against the original URDF / spec FK)
116
+ - `refinement_used` — `"none"` or `"lm"` if Levenberg–Marquardt polish fired
117
+
118
+ A single 6-DOF target pose admits up to **16 analytical IK branches** (8 typical for a Pieper-class arm: 4 shoulder × 2 elbow, with the wrist deterministic). For 7R redundant arms the IK is a 1-parameter family; ssik discretises it into 32–256 branches per pose depending on the swivel-sample count.
119
+
120
+ By default `solve()` runs **`respect_limits=True`**: out-of-URDF-limit branches are dropped (with a `q ± 2π` rescue pass first). On 7R jointlock arms the limits filter runs *during* the lock-sweep so `max_solutions=1` short-circuits on the first in-limits candidate rather than wasting samples on branches the postprocess would discard. Pass `respect_limits=False` for the raw geometric set.
121
+
122
+ The `allow_refinement=True` opt-in runs LM polish per algebraic candidate at a few hundred microseconds per branch — useful when an algebraic candidate lands just above `fk_atol` near a kinematic singularity.
123
+
124
+ ## Tuning knobs
125
+
126
+ ### `TolerancePolicy` — six thresholds, one object
127
+
128
+ `solve()` accepts an optional `policy=` kwarg. The default `ssik.DEFAULT_TOLERANCE_POLICY` works for every shipped fixture; reach for a custom policy when a real arm's URDF has structural near-degeneracies (axes that *almost* but not exactly meet) or when you want tighter / looser FK closure than the defaults provide.
129
+
130
+ ```python
131
+ from ssik import TolerancePolicy, DEFAULT_TOLERANCE_POLICY
132
+
133
+ policy = TolerancePolicy(
134
+ axis_parallel=1e-8, # ||a × b||: when two axes are "parallel"
135
+ axis_intersect=1e-8, # perpendicular distance: when two lines "meet"
136
+ subproblem_feasibility=1e-9,# is_ls boundary inside SP1-SP6
137
+ subproblem_numerical=1e-5, # FK-closure filter on algebraic candidates
138
+ subproblem_degeneracy=1e-12,# rank-drop threshold; below this, return []
139
+ subproblem_dedup=1e-3, # angle-space tolerance for collapsing duplicates
140
+ )
141
+ sols = my_arm_ik.solve(T_target, policy=policy)
142
+ ```
143
+
144
+ The fields are named for *why* they exist so log messages can say `"SP6 sign branch rejected: closure 1.2e-4 > subproblem_numerical 1e-5"` instead of citing magic numbers.
145
+
146
+ ### `ssik.postprocess` — composable filters
147
+
148
+ `solve()` returns the geometric IK set. For application-specific filtering, four helpers in `ssik.postprocess` compose into the typical "robot-aware IK" pipeline:
149
+
150
+ ```python
151
+ from ssik.postprocess import (
152
+ respect_limits, wrap_to_limits, nearest_to_seed, take_first,
153
+ )
154
+
155
+ sols = my_arm_ik.solve(T_target, respect_limits=False) # raw geometric set
156
+ sols = wrap_to_limits(sols, my_arm_ik._KB) # try q ± 2π to bring in
157
+ sols = respect_limits(sols, my_arm_ik._KB) # drop anything still outside
158
+ sols = nearest_to_seed(sols, q_current) # sort by wrap-to-pi distance
159
+ sols = take_first(sols, k=4) # top-k after ranking
160
+ ```
161
+
162
+ By default `solve()` already runs `wrap_to_limits` + `respect_limits`; the standalone helpers exist for callers who want a different order, a different metric, or to add their own filters (collision-aware filtering, dexterity scoring, custom seed metrics) between the layers.
163
+
164
+ Out of scope: collision filtering (use FCL or similar at the application layer) and continuous-trajectory smoothness (typically a separate planner concern).
165
+
166
+ ## How it compares
167
+
168
+ Numerical-IK libraries take a seed, run damped least-squares to a **single** converged configuration, and stop. ssik returns **every analytical branch** at machine precision. Branch enumeration matters for motion planning (try every branch, pick the one with best clearance), for dexterity analysis (the manipulability ellipsoid is per-branch), and for trajectory continuation across kinematic singularities.
169
+
170
+ EAIK (Ostermeier 2024) is the canonical Python wrapper around C++ subproblem-decomposition solvers. It's analytical on the kinematic families it recognises and refuses everything else. Numbers below from [`examples/04_compare_vs_eaik.py`](examples/04_compare_vs_eaik.py) over 100 random reachable poses per arm, Apple M3 single-thread, mean ± 95% CI via 1000-resample bootstrap. FK residual is the Frobenius norm `‖FK(q) − T‖` against the original URDF / spec FK.
171
+
172
+ | Arm (class) | EAIK | ssik |
173
+ |---|---|---|
174
+ | UR5 (Pieper 6R, three-parallel) | 5 ± 0 µs / FK 2e-15 / 4 sols | 556 ± 12 µs / FK 2e-9 / 4 sols |
175
+ | Puma 560 (Pieper 6R, spherical wrist) | 5 ± 1 µs / FK 3e-14 / 8 sols | 245 ± 3 µs / FK 2e-14 / 8 sols |
176
+ | JACO 2 (**non-Pieper 6R**) | **refuses** ("6R-Unknown Kinematic Class") | 1.02 ± 0.04 ms / FK 3e-6 / 2 sols |
177
+ | iiwa14 (SRS 7R) | **refuses** ("only 1-6R robots are solvable") | 5.10 ± 0.07 ms / FK 4e-13 / 96 sols |
178
+ | Gen3 (**approximate-SRS 7R**, 12 mm offset) | **refuses** ("only 1-6R") | 41.83 ± 1.15 ms / FK 1e-12 / 47 sols |
179
+ | Franka Panda (anthropomorphic 7R) | **refuses** ("only 1-6R") | 28.03 ± 2.57 ms / FK 7e-13 / 9 sols |
180
+ | Rizon 4 (**non-SRS 7R**) | **refuses** ("only 1-6R") | 30.88 ± 8.80 ms / FK 4e-9 / 35 sols |
181
+ | Kassow KR810 (**non-SRS 7R**) | **refuses** ("only 1-6R") | 28.06 ± 10.63 ms / FK 7e-8 / 24 sols |
182
+
183
+ EAIK is ~100× faster than ssik on Pieper-class 6R — that is its native sweet spot, and ssik does not try to compete there. The interesting cells are the **refuses** ones: non-Pieper 6R (JACO 2) and every 7R arm. Those are the geometries ssik exists for. The "refuses (...)" strings are EAIK's actual error messages, captured verbatim by the bench harness. A numerical-IK comparison (MINK) is tracked separately in [#236](https://github.com/personalrobotics/ssik/issues/236).
184
+
185
+ ## Under the hood
186
+
187
+ The algorithmic ingredients are not novel — Raghavan–Roth (1990), Manocha–Canny (1994), Singh–Kreutz (1989), Husty–Pfurner (2007). What's new is making the textbook pipelines survive on real ill-conditioned arms (AE-3 leftvar selection on JACO 2 drops `cond(m_quad)` from 3.75 × 10^16 to 127), composing them with a uniform dispatch layer, and packaging the whole thing as a deployable artifact.
188
+
189
+ Cython hot loops cover the leaf primitives (POE forward kinematics, the Levenberg–Marquardt polish and analytical Jacobian); the rest is pure Python so it stays inspectable.
190
+
191
+ **Bulletproof testing**: every solver lands with N-way cross-solver agreement on shared fixtures, FK closure ≤ 1e-10 on every retained IK, 500+ Hypothesis-fuzzed random poses per fixture, and an explicit speed bench that has to clear a regression gate. The current suite has **1300+ tests across 11 fixture arms**. Negative-result spikes (a Cython estimate that misses by 2-5×, a codegen-bake on a part that's 0.3% of runtime) are published as closed issues with profile data so the next contributor doesn't repeat the path.
192
+
193
+ ## Documentation
194
+
195
+ - [docs/arm_coverage.md](docs/arm_coverage.md) — per-arm fixture tables, tested speeds, source URDFs
196
+ - [docs/architecture.md](docs/architecture.md) — solver tier catalog, dispatch flow, build artifact internals, algorithmic lineage
197
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — repo layout, dev setup, testing discipline
198
+
199
+ ## Related libraries
200
+
201
+ ssik does not compete with these on the arms they cover. Pick the right tool for your geometry.
202
+
203
+ - [**EAIK**](https://github.com/OstermD/EAIK) (Ostermeier 2024) — Python wrapper around C++ subproblem-decomposition solvers. Analytical, returns all branches on Pieper-class 6R and canonical SRS 7R (with a manual joint lock). Refuses arms outside its recognised kinematic families. Directly benchmarked in the table above.
204
+ - [**IK-Geo**](https://github.com/rpiRobotics/ik-geo) (Elias–Wen 2022/2025) — the reference C++/Rust implementation of subproblem decomposition. Same coverage profile as EAIK. Has Python bindings (`ik-geo` on PyPI); currently pins `pyo3==0.20.3` so the wheel is incompatible with Python 3.13 — track upstream for an update.
205
+ - [**IKFast**](http://openrave.org/docs/latest_stable/openravepy/ikfast/) (Diankov 2010, part of OpenRAVE) — the original analytical-IK codegen tool. Symbolic preprocessing in sympy → per-arm C++. Works well on the kinematic families it was tuned for (Pieper-class 6R, spherical-wrist 7R via joint lock); the symbolic pipeline fails on modern sympy for non-Pieper geometries (`mpmath.polyroots` NoConvergence, `Matrix.inv` / `Matrix.det` stalls). LGPL-licensed.
206
+ - [**MINK**](https://github.com/kevinzakka/mink) (Zakka) — Mujoco-native numerical IK via damped least-squares. Iterative, takes a seed, converges to a single configuration. Handles any kinematic geometry but returns one IK, not all branches, and FK closure is proportional to the convergence tolerance (typically 1e-3 to 1e-6 rather than machine precision).
207
+ - [**TracIK**](https://traclabs.com/projects/trac-ik/) (Beeson & Ames 2015) — combined SQP / pseudoinverse Jacobian solver; the ROS Industrial default numerical IK. URDF-native. Same one-branch-per-seed semantics as MINK. The maintained Python binding (`pytracik`) ships a broken arm64 wheel; the ROS-native binding works fine inside ROS.
208
+ - [**KDL-LMA**](https://github.com/orocos/orocos_kinematics_dynamics) — OROCOS KDL's Levenberg-Marquardt numerical IK. Older and less robust than TracIK or MINK on the same problem class.
209
+
210
+ ## License
211
+
212
+ [BSD-3-Clause](LICENSE). The library incorporates clean-room reimplementations of algorithms from BSD-3-licensed IK-Geo (Elias–Wen 2022/2025) and from the academic publications of Raghavan–Roth (1990), Manocha–Canny (1994), Singh–Kreutz (1989), and Husty–Pfurner (2007). Algorithmic lineage is documented in module docstrings.
213
+
214
+ ## Citation
215
+
216
+ ```bibtex
217
+ @software{ssik,
218
+ author = {Srinivasa, Siddhartha},
219
+ title = {ssik: analytical inverse kinematics for 6R and 7R revolute arms},
220
+ url = {https://github.com/personalrobotics/ssik},
221
+ year = {2026},
222
+ }
223
+ ```