Skip to content

Commit ba0022c

Browse files
committed
feat: python interface
- python interface - extended readme with example - added example to tests - made q0 optional in cpp code - bind home pose
1 parent 2cf411b commit ba0022c

File tree

6 files changed

+163
-17
lines changed

6 files changed

+163
-17
lines changed

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@
44

55
`frankik` is a standalone Python library that implements the analytical geometric IK solver proposed by **He & Liu (2021)**. It is designed for researchers and developers who need high-performance kinematics without the overhead of ROS, MoveIt, or hardware drivers.
66

7+
## Example
8+
```python
9+
import numpy as np
10+
from frankik import FrankaKinematics, RobotType
11+
kinematics = FrankaKinematics(robot_type=RobotType.FR3) # Robot.Type.PANDA also supported
12+
q_home = np.array([
13+
0.0,
14+
-np.pi / 4,
15+
0.0,
16+
-3 * np.pi / 4,
17+
0.0,
18+
np.pi / 2,
19+
np.pi / 4
20+
])
21+
pose_home = kinematics.forward(q_home)
22+
q = kinematics.inverse(pose_home)
23+
assert np.allclose(q, q_home)
24+
print(q)
25+
```
26+
727
## Installation from source
828
```shell
929
sudo apt install libeigen3-dev
@@ -20,7 +40,7 @@ Coming soon...
2040
## Feature List
2141
- [x] basic bindings from he
2242
- [x] support for fr3
23-
- [ ] nice python interface
24-
- [ ] tests
43+
- [x] nice python interface
44+
- [x] tests
2545
- [ ] performance benchmarks
2646

src/frankik/__init__.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,121 @@
1+
import typing
2+
3+
import numpy as np
4+
15
from frankik._core import (
26
__version__,
37
fk,
48
ik,
59
ik_full,
10+
kQDefault,
611
q_max_fr3,
712
q_max_panda,
813
q_min_fr3,
914
q_min_panda,
1015
)
1116

12-
__all__ = ["__version__", "ik", "ik_full", "fk", "q_max_fr3", "q_max_panda", "q_min_fr3", "q_min_panda"]
17+
18+
class RobotType:
19+
PANDA = "panda"
20+
FR3 = "fr3"
21+
22+
23+
class FrankaKinematics:
24+
25+
def __init__(self, robot_type: str = RobotType.FR3):
26+
"""Initialize Franka Kinematics for the specified robot type.
27+
Args:
28+
robot_type (str): Type of the robot, either 'panda' or 'fr3'.
29+
Raises:
30+
ValueError: If an unsupported robot type is provided.
31+
"""
32+
if robot_type not in (RobotType.PANDA, RobotType.FR3):
33+
raise ValueError(f"Unsupported robot type: {robot_type}. Choose 'panda' or 'fr3'.")
34+
self.robot_type = robot_type
35+
self.q_min = q_min_fr3 if robot_type == RobotType.FR3 else q_min_panda
36+
self.q_max = q_max_fr3 if robot_type == RobotType.FR3 else q_max_panda
37+
self.q_home = kQDefault
38+
39+
@staticmethod
40+
def pose_inverse(
41+
T: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]]
42+
) -> np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]]:
43+
"""Compute the inverse of a homogeneous transformation matrix.
44+
Args:
45+
T (np.ndarray): A 4x4 homogeneous transformation matrix.
46+
Returns:
47+
np.ndarray: The inverse of the input transformation matrix.
48+
"""
49+
R = T[:3, :3]
50+
t = T[:3, 3]
51+
T_inv = np.eye(4)
52+
T_inv[:3, :3] = R.T
53+
T_inv[:3, 3] = -R.T @ t
54+
return T_inv
55+
56+
def forward(
57+
self,
58+
q0: np.ndarray[tuple[typing.Literal[7]], np.dtype[np.float64]],
59+
tcp_offset: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]] | None = None,
60+
) -> np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]]:
61+
"""Compute the forward kinematics for the given joint configuration.
62+
Args:
63+
q0 (np.ndarray): A 7-element array representing joint angles.
64+
tcp_offset (np.ndarray, optional): A 4x4 homogeneous transformation matrix representing
65+
the tool center point offset. Defaults to None.
66+
Returns:
67+
np.ndarray: A 4x4 homogeneous transformation matrix representing the end-effector pose.
68+
"""
69+
pose = fk(q0)
70+
return pose @ self.pose_inverse(tcp_offset) if tcp_offset is not None else pose
71+
72+
def inverse(
73+
self,
74+
pose: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]],
75+
q0: np.ndarray[tuple[typing.Literal[7]], np.dtype[np.float64]] | None = None,
76+
tcp_offset: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]] | None = None,
77+
allow_elbow_flips: bool = False,
78+
) -> np.ndarray[tuple[typing.Literal[7]], np.dtype[np.float64]] | None:
79+
"""Compute the inverse kinematics for the given end-effector pose.
80+
Args:
81+
pose (np.ndarray): A 4x4 homogeneous transformation matrix representing the desired end-effector pose.
82+
q0 (np.ndarray, optional): A 7-element array representing the current joint angles. Defaults to None.
83+
tcp_offset (np.ndarray, optional): A 4x4 homogeneous transformation matrix representing
84+
the tool center point offset. Defaults to None.
85+
allow_elbow_flips (bool): Whether to consider multiple IK solutions (elbow flips). Defaults to False.
86+
Returns:
87+
np.ndarray | None: A 7-element array representing the joint angles if a solution is found; otherwise, None.
88+
"""
89+
new_pose = pose @ self.pose_inverse(tcp_offset) if tcp_offset is not None else pose
90+
q = ik(new_pose, q0, is_fr3=(self.robot_type == RobotType.FR3))
91+
92+
if not np.isnan(q).any():
93+
return q
94+
if not allow_elbow_flips:
95+
return None
96+
qs = ik_full(new_pose, q0, is_fr3=(self.robot_type == RobotType.FR3))
97+
qs_list = list(qs)
98+
# drop nan solutions
99+
qs_list = [q for q in qs_list if not np.isnan(q).any()]
100+
if len(qs_list) == 0:
101+
return None
102+
if q0 is None:
103+
q0 = self.q_home
104+
105+
# find the closest solution
106+
best_q = min(qs_list, key=lambda q_sol: np.linalg.norm(q_sol - q0))
107+
return best_q
108+
109+
110+
__all__ = [
111+
"__version__",
112+
"ik",
113+
"ik_full",
114+
"fk",
115+
"q_max_fr3",
116+
"q_max_panda",
117+
"q_min_fr3",
118+
"q_min_panda",
119+
"FrankaKinematics",
120+
"RobotType",
121+
]

src/frankik/_core.pyi

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import typing
88

99
import numpy
1010

11-
__all__: list[str] = ["fk", "ik", "ik_full", "q_max_fr3", "q_max_panda", "q_min_fr3", "q_min_panda"]
11+
__all__: list[str] = ["fk", "ik", "ik_full", "kQDefault", "q_max_fr3", "q_max_panda", "q_min_fr3", "q_min_panda"]
1212

1313
def fk(
1414
q: numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]]
@@ -25,7 +25,7 @@ def fk(
2525

2626
def ik(
2727
O_T_EE: numpy.ndarray[tuple[typing.Literal[4], typing.Literal[4]], numpy.dtype[numpy.float64]],
28-
q_actual_array: numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]] = ...,
28+
q_actual: numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]] | None = None,
2929
q7: float = 0.7853981633974483,
3030
is_fr3: bool = False,
3131
) -> numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]]:
@@ -34,7 +34,7 @@ def ik(
3434
3535
Args:
3636
O_T_EE (Eigen::Matrix<double, 4, 4>): Desired end-effector pose.
37-
q_actual_array (Vector7d, optional): Current joint angles. Defaults to kQDefault.
37+
q_actual (Vector7d, optional): Current joint angles. Defaults to kQDefault.
3838
q7 (double, optional): Joint 7 angle. Defaults to M_PI_4.
3939
is_fr3 (bool, optional): Whether to use FR3 joint limits. Defaults to false.
4040
@@ -44,7 +44,7 @@ def ik(
4444

4545
def ik_full(
4646
O_T_EE: numpy.ndarray[tuple[typing.Literal[4], typing.Literal[4]], numpy.dtype[numpy.float64]],
47-
q_actual_array: numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]] = ...,
47+
q_actual: numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]] | None = None,
4848
q7: float = 0.7853981633974483,
4949
is_fr3: bool = False,
5050
) -> numpy.ndarray[tuple[typing.Literal[4], typing.Literal[7]], numpy.dtype[numpy.float64]]:
@@ -53,7 +53,7 @@ def ik_full(
5353
5454
Args:
5555
O_T_EE (Eigen::Matrix<double, 4, 4>): Desired end-effector pose.
56-
q_actual_array (Vector7d, optional): Current joint angles. Defaults to kQDefault.
56+
q_actual (Vector7d, optional): Current joint angles. Defaults to kQDefault.
5757
q7 (double, optional): Joint 7 angle. Defaults to M_PI_4.
5858
is_fr3 (bool, optional): Whether to use FR3 joint limits. Defaults to false.
5959
@@ -62,6 +62,7 @@ def ik_full(
6262
"""
6363

6464
__version__: str = "0.1"
65+
kQDefault: numpy.ndarray # value = array([ 0. , -0.78539816, 0. , -2.35619449, 0. ,...
6566
q_max_fr3: list = [2.3093, 1.5133, 2.4937, -0.4461, 2.48, 4.2094, 2.6895]
6667
q_max_panda: list = [2.8973, 1.7628, 2.8973, -0.0698, 2.8973, 3.7525, 2.8973]
6768
q_min_fr3: list = [-2.3093, -1.5133, -2.4937, -2.7478, -2.48, 0.8521, -2.6895]

src/pybind/bindings.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ PYBIND11_MODULE(_core, m) {
2727
m.attr("q_max_panda") = frankik::q_max_panda;
2828
m.attr("q_min_fr3") = frankik::q_min_fr3;
2929
m.attr("q_max_fr3") = frankik::q_max_fr3;
30+
m.attr("kQDefault") = frankik::kQDefault;
3031

3132
m.def("ik_full", &frankik::ik_full, py::arg("O_T_EE"),
32-
py::arg("q_actual_array") = frankik::kQDefault, py::arg("q7") = M_PI_4,
33+
py::arg("q_actual") = std::nullopt, py::arg("q7") = M_PI_4,
3334
py::arg("is_fr3") = false,
3435
"Compute full inverse kinematics for Franka Emika Panda robot.\n\n"
3536
"Args:\n"
3637
" O_T_EE (Eigen::Matrix<double, 4, 4>): Desired end-effector pose.\n"
37-
" q_actual_array (Vector7d, optional): Current joint angles. "
38+
" q_actual (Vector7d, optional): Current joint angles. "
3839
"Defaults to kQDefault.\n"
3940
" q7 (double, optional): Joint 7 angle. Defaults to M_PI_4.\n"
4041
" is_fr3 (bool, optional): Whether to use FR3 joint limits. "
@@ -43,13 +44,13 @@ PYBIND11_MODULE(_core, m) {
4344
" Eigen::Matrix<double, 4, 7>: All possible IK solutions (up to 4). "
4445
"NaN if no solution.");
4546
m.def("ik", &frankik::ik, py::arg("O_T_EE"),
46-
py::arg("q_actual_array") = frankik::kQDefault, py::arg("q7") = M_PI_4,
47+
py::arg("q_actual") = std::nullopt, py::arg("q7") = M_PI_4,
4748
py::arg("is_fr3") = false,
4849
"Compute one inverse kinematics solution for Franka Emika Panda "
4950
"robot.\n\n"
5051
"Args:\n"
5152
" O_T_EE (Eigen::Matrix<double, 4, 4>): Desired end-effector pose.\n"
52-
" q_actual_array (Vector7d, optional): Current joint angles. "
53+
" q_actual (Vector7d, optional): Current joint angles. "
5354
"Defaults to kQDefault.\n"
5455
" q7 (double, optional): Joint 7 angle. Defaults to M_PI_4.\n"
5556
" is_fr3 (bool, optional): Whether to use FR3 joint limits. "

src/pybind/frankik.h

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ const Vector7d kQDefault =
4141
(Vector7d() << 0.0, -M_PI_4, 0.0, -3 * M_PI_4, 0.0, M_PI_2, M_PI_4)
4242
.finished();
4343

44-
Eigen::Matrix<double, 4, 7> ik_full(Eigen::Matrix<double, 4, 4> O_T_EE,
45-
Vector7d q_actual_array = kQDefault,
46-
double q7 = M_PI_4, bool is_fr3 = false) {
47-
44+
Eigen::Matrix<double, 4, 7>
45+
ik_full(Eigen::Matrix<double, 4, 4> O_T_EE,
46+
std::optional<Vector7d> q_actual = std::nullopt, double q7 = M_PI_4,
47+
bool is_fr3 = false) {
48+
Vector7d q_actual_array = q_actual.value_or(kQDefault);
4849
const Eigen::Matrix<double, 4, 7> q_all_NAN =
4950
Eigen::Matrix<double, 4, 7>::Constant(
5051
std::numeric_limits<double>::quiet_NaN());
@@ -221,8 +222,9 @@ Eigen::Matrix<double, 4, 7> ik_full(Eigen::Matrix<double, 4, 4> O_T_EE,
221222
}
222223

223224
Vector7d ik(Eigen::Matrix<double, 4, 4> O_T_EE,
224-
Vector7d q_actual_array = frankik::kQDefault, double q7 = M_PI_4,
225+
std::optional<Vector7d> q_actual = std::nullopt, double q7 = M_PI_4,
225226
bool is_fr3 = false) {
227+
Vector7d q_actual_array = q_actual.value_or(kQDefault);
226228
const Vector7d q_NAN =
227229
Vector7d::Constant(std::numeric_limits<double>::quiet_NaN());
228230
Vector7d q;

src/tests/test_example.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import numpy as np
2+
import pytest
3+
4+
from frankik import FrankaKinematics, RobotType
5+
6+
7+
@pytest.mark.parametrize("robot_type", [RobotType.PANDA, RobotType.FR3])
8+
def test_example(robot_type):
9+
kinematics = FrankaKinematics(robot_type=robot_type)
10+
q_home = np.array([0.0, -np.pi / 4, 0.0, -3 * np.pi / 4, 0.0, np.pi / 2, np.pi / 4])
11+
pose_home = kinematics.forward(q_home)
12+
q = kinematics.inverse(pose_home)
13+
assert np.allclose(q, q_home)

0 commit comments

Comments
 (0)