Skip to content

Commit 4a67782

Browse files
authored
feat: global ik with q7 sampling (#3)
- added global ik with q7 sampling - moved ik sampling solution selection logic to python - added joint weights
1 parent 2c7eaf1 commit 4a67782

File tree

5 files changed

+84
-48
lines changed

5 files changed

+84
-48
lines changed

src/frankik/__init__.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import typing
2-
31
import numpy as np
42

53
from frankik._core import (
@@ -48,9 +46,7 @@ def __init__(self, robot_type: str = RobotType.FR3):
4846
self.q_home = kQDefault
4947

5048
@staticmethod
51-
def pose_inverse(
52-
T: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]]
53-
) -> np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]]:
49+
def pose_inverse(T: np.ndarray) -> np.ndarray:
5450
"""Compute the inverse of a homogeneous transformation matrix.
5551
Args:
5652
T (np.ndarray): A 4x4 homogeneous transformation matrix.
@@ -66,9 +62,9 @@ def pose_inverse(
6662

6763
def forward(
6864
self,
69-
q0: np.ndarray[tuple[typing.Literal[7]], np.dtype[np.float64]],
70-
tcp_offset: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]] | None = None,
71-
) -> np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]]:
65+
q0: np.ndarray,
66+
tcp_offset: np.ndarray | None = None,
67+
) -> np.ndarray:
7268
"""Compute the forward kinematics for the given joint configuration.
7369
Args:
7470
q0 (np.ndarray): A 7-element array representing joint angles.
@@ -83,11 +79,15 @@ def forward(
8379

8480
def inverse(
8581
self,
86-
pose: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]],
87-
q0: np.ndarray[tuple[typing.Literal[7]], np.dtype[np.float64]] | None = None,
88-
tcp_offset: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]] | None = None,
82+
pose: np.ndarray,
83+
q0: np.ndarray | None = None,
84+
tcp_offset: np.ndarray | None = None,
8985
q7: float | None = None,
90-
) -> np.ndarray[tuple[typing.Literal[7]], np.dtype[np.float64]] | None:
86+
global_solution: bool = False,
87+
joint_weight: np.ndarray | None = None,
88+
q7_sample_interval=40,
89+
q7_sample_size=60,
90+
) -> np.ndarray | None:
9191
"""Compute the inverse kinematics for the given end-effector pose.
9292
9393
Args:
@@ -96,15 +96,33 @@ def inverse(
9696
tcp_offset (np.ndarray, optional): A 4x4 homogeneous transformation matrix representing
9797
the tool center point offset. Defaults to None.
9898
q7 (float, optional): The angle of the seventh joint, used for FR3 robot IK. If None then it will be sampled. Defaults to None.
99+
global_solution (bool, optional): Whether to consider global ik solutions. Defaults to False.
100+
joint_weight (np.ndarray, optional): Weights for calculating the distance between the solution and q0. Defaults to None.
101+
q7_sample_interval (int, optional): The interval for sampling q7. Defaults to 40.
102+
q7_sample_size (int, optional): The number of samples for q7. Defaults to 60.
99103
100104
Returns:
101105
np.ndarray | None: A 7-element array representing the joint angles if a solution is found; otherwise, None.
102106
"""
107+
if joint_weight is None:
108+
joint_weight = np.ones(7)
109+
if q0 is None:
110+
q0 = self.q_home
111+
103112
new_pose = pose @ self.pose_inverse(tcp_offset) if tcp_offset is not None else pose
104113
if q7 is not None:
105114
q = ik(new_pose, q0, q7, is_fr3=(self.robot_type == RobotType.FR3)) # type: ignore
106115
else:
107-
q = ik_sample_q7(new_pose, q0, is_fr3=(self.robot_type == RobotType.FR3)) # type: ignore
116+
qs = ik_sample_q7(
117+
new_pose,
118+
q0,
119+
is_fr3=(self.robot_type == RobotType.FR3),
120+
sample_size=q7_sample_size,
121+
sample_interval=q7_sample_interval,
122+
full_ik=global_solution,
123+
) # type: ignore
124+
q_diffs = np.sum((np.array(qs) - q0) * joint_weight, axis=1) ** 2
125+
q = qs[np.argmin(q_diffs)]
108126

109127
if np.isnan(q).any():
110128
return None

src/frankik/_core.pyi

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,23 +73,27 @@ def ik_full(
7373

7474
def ik_sample_q7(
7575
O_T_EE: numpy.ndarray[tuple[typing.Literal[4], typing.Literal[4]], numpy.dtype[numpy.float64]],
76-
q_actual: numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]],
76+
q_actual: numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]] | None = None,
7777
is_fr3: bool = False,
78-
sample_size: int = 30,
79-
sample_interval: float = 10,
80-
) -> numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]]:
78+
sample_size: int = 60,
79+
sample_interval: float = 40,
80+
full_ik: bool = False,
81+
) -> list[numpy.ndarray[tuple[typing.Literal[7]], numpy.dtype[numpy.float64]]]:
8182
"""
8283
Compute one inverse kinematics solution for Franka with sampling of joint q7.
8384
8485
Args:
85-
O_T_EE (Eigen::Matrix<double, 4, 4>): Desired end-effector pose.
86-
q_actual (Vector7d): Current joint angles.
87-
is_fr3 (bool, optional): Whether to use FR3 joint limits. Defaults to false.
86+
O_T_EE (np.array): Desired end-effector pose. Shape (4, 4).
87+
q_actual (np.array, optional): Current joint angles. Shape (7,).
88+
89+
is_fr3 (bool, optional): Whether to use FR3 joint limits. Defaults to False.
8890
sample_size (int, optional): How many sample to try for q7. Defaults to 20.
8991
sample_interval (int, optional): Sample interval for q7 in degree. Defaults to 90.
92+
full_ik (bool, optional): Whether to use full IK. Defaults to False.
93+
degree. Defaults to False.
9094
9195
Returns:
92-
Vector7d: One IK solution. NaN if no solution.
96+
list[np.array]: One IK solution. Empty if no solution was found. Array shape (7,).
9397
"""
9498

9599
__version__: str = "0.1"

src/pybind/bindings.cpp

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,29 @@ PYBIND11_MODULE(_core, m) {
5858
"Returns:\n"
5959
" Vector7d: One IK solution. NaN if no solution.");
6060
m.def("ik_sample_q7", &frankik::ik_sample_q7, py::arg("O_T_EE"),
61-
py::arg("q_actual"), py::arg("is_fr3") = false,
62-
py::arg("sample_size") = 30, py::arg("sample_interval") = 10,
61+
py::arg("q_actual") = std::nullopt, py::arg("is_fr3") = false,
62+
py::arg("sample_size") = 60, py::arg("sample_interval") = 40,
63+
py::arg("full_ik") = false,
6364
"Compute one inverse kinematics solution for Franka with sampling of "
6465
"joint q7.\n\n"
6566
"Args:\n"
66-
" O_T_EE (Eigen::Matrix<double, 4, 4>): Desired end-effector pose.\n"
67-
" q_actual (Vector7d): Current joint angles.\n"
67+
" O_T_EE (np.array): Desired end-effector pose. Shape "
68+
"(4, 4).\n"
69+
" q_actual (np.array, optional): Current joint angles. Shape "
70+
"(7,).\n"
71+
"\n"
6872
" is_fr3 (bool, optional): Whether to use FR3 joint limits. "
69-
"Defaults to false.\n"
73+
"Defaults to False.\n"
7074
" sample_size (int, optional): How many sample to try for q7. "
7175
"Defaults to 20.\n"
7276
" sample_interval (int, optional): Sample interval for q7 in "
73-
"degree. Defaults to 90.\n\n"
77+
"degree. Defaults to 90.\n"
78+
" full_ik (bool, optional): Whether to use full IK. Defaults to "
79+
"False.\n"
80+
"degree. Defaults to False.\n\n"
7481
"Returns:\n"
75-
" Vector7d: One IK solution. NaN if no solution.");
82+
" list[np.array]: One IK solution. Empty if no solution was found. "
83+
"Array shape (7,).");
7684
m.def("fk", &frankik::fk, py::arg("q"),
7785
"Compute forward kinematics for Franka robot.\n\n"
7886
"Args:\n"

src/pybind/frankik.h

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -396,23 +396,23 @@ Vector7d ik(Eigen::Matrix<double, 4, 4> O_T_EE,
396396
return q;
397397
}
398398

399-
Vector7d ik_sample_q7(Eigen::Matrix<double, 4, 4> O_T_EE, Vector7d q_actual,
400-
bool is_fr3 = false, const size_t sample_size = 30,
401-
const double sample_interval = 10) {
399+
std::vector<Vector7d> ik_sample_q7(
400+
const Eigen::Matrix<double, 4, 4> O_T_EE,
401+
const std::optional<Vector7d> q_actual = std::nullopt, bool is_fr3 = false,
402+
size_t sample_size = 30, double sample_interval = 10,
403+
bool full_ik = false) {
404+
Vector7d q_actual_array = q_actual.value_or(kQDefault);
402405
const std::array<double, 7> q_min = is_fr3 ? q_min_fr3 : q_min_panda;
403406
const std::array<double, 7> q_max = is_fr3 ? q_max_fr3 : q_max_panda;
404407

405-
auto q7 = q_actual[6];
408+
auto q7 = q_actual_array[6];
406409

407410
// sample interval in degree
408411
auto low7 = q7 - (sample_interval / 2.0) / 180.0 * M_PI;
409412
auto high7 = q7 + (sample_interval / 2.0) / 180.0 * M_PI;
410413

411414
auto sample_step_size = (high7 - low7) / sample_size;
412-
413-
Vector7d ik_solution =
414-
Vector7d::Constant(std::numeric_limits<double>::quiet_NaN());
415-
double loss = std::numeric_limits<double>::max();
415+
std::vector<Vector7d> ik_solutions;
416416

417417
// shift if interval goes over the edges
418418
if (low7 < q_min[6]) {
@@ -426,18 +426,24 @@ Vector7d ik_sample_q7(Eigen::Matrix<double, 4, 4> O_T_EE, Vector7d q_actual,
426426
}
427427
for (size_t i = 0; i <= sample_size; ++i) {
428428
// last step is to try the current q7 position
429-
q7 = i < sample_size ? low7 + sample_step_size * i : q_actual[6];
430-
auto tmp_ik_sol = ik(O_T_EE, q_actual, q7, is_fr3);
431-
if (std::isnan(tmp_ik_sol[0])) {
432-
continue;
433-
}
434-
auto tmp_loss = (tmp_ik_sol - q_actual).norm();
435-
if (tmp_loss < loss) {
436-
loss = tmp_loss;
437-
ik_solution = tmp_ik_sol;
429+
q7 = i < sample_size ? low7 + sample_step_size * i : q_actual_array[6];
430+
if (!full_ik) {
431+
auto tmp_ik_sol = ik(O_T_EE, q_actual_array, q7, is_fr3);
432+
if (std::isnan(tmp_ik_sol[0])) {
433+
continue;
434+
}
435+
ik_solutions.push_back(tmp_ik_sol);
436+
} else {
437+
auto tmp_ik_sols = ik_full(O_T_EE, q_actual_array, q7, is_fr3);
438+
for (size_t j = 0; j <= 4; ++j) {
439+
if (std::isnan(tmp_ik_sols.row(j)[0])) {
440+
continue;
441+
}
442+
ik_solutions.push_back(tmp_ik_sols.row(j));
443+
}
438444
}
439445
}
440-
return ik_solution;
446+
return ik_solutions;
441447
}
442448

443449
Eigen::Matrix<double, 4, 4> fk(const Eigen::Matrix<double, 7, 1>& q) {

src/tests/test_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
def test_example(robot_type):
99
kinematics = FrankaKinematics(robot_type=robot_type)
1010
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, tcp_offset=kinematics.FrankaHandTCPOffset) # type: ignore
12-
q = kinematics.inverse(pose_home, tcp_offset=kinematics.FrankaHandTCPOffset, q0=q_home) # type: ignore
11+
pose_home = kinematics.forward(q_home, tcp_offset=kinematics.FrankaHandTCPOffset)
12+
q = kinematics.inverse(pose_home, tcp_offset=kinematics.FrankaHandTCPOffset, q0=q_home)
1313
assert np.allclose(q, q_home) # type: ignore

0 commit comments

Comments
 (0)