Compare commits

...

6 Commits

Author SHA1 Message Date
dfaeb95282 update readme 2026-07-01 15:58:39 +01:00
7246710a7d 1. add q_mid and mid weight, making the joint prefer to stay at the middle of joint range.
2. add dq_limit weight, making the last several joints move more proactively.
2026-07-01 15:42:21 +01:00
4b5eeccf7f Update README.md 2026-06-22 23:21:49 +08:00
f1846ffe1e Example for using qp based ik with urdf, and realman official ik 2026-06-22 16:19:24 +01:00
58452bce90 start to adjust to ready-to-use class 2026-06-22 13:53:12 +01:00
6c8a335e1d start to adjust to ready-to-use class 2026-06-22 13:29:58 +01:00
8 changed files with 172 additions and 102 deletions

View File

@ -1,5 +1,7 @@
### This repo is for inverse kinematics and verification ### This repo is for inverse kinematics and verification
In this branch, the qp-based inverse kinematics method is modified as a python class. The user can call it as in `main.py`
Inverse Kinematics (IK) is numerically obtained through quadratic programming (QP). Inverse Kinematics (IK) is numerically obtained through quadratic programming (QP).
Verification is done with Mujoco simulation. Verification is done with Mujoco simulation.
@ -32,3 +34,15 @@ lb = -ub
the success rates for **qp-based ik** and **realman Algo ik** are **76%** and **51%**.\ the success rates for **qp-based ik** and **realman Algo ik** are **76%** and **51%**.\
At least one solver works out the ik, rate = **84%**. At least one solver works out the ik, rate = **84%**.
### update(1st July 2026)
In each iteration, update optimization formula:
- new cost item for distance from middle of the joint range.
- set up different weight for different joints motion.
<img src="img/optimization.png" alt="Cost" width="400">
<img src="img/cons.png" alt="Cost" width="400">
<img src="img/osqp.png" alt="Cost" width="400">

BIN
img/cons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
img/optimization.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
img/osqp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

6
kine_ctrl/fix_robotics_env.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
echo "Fixing robotics environment..."
conda activate coppeliasim
export PYTHONPATH="/home/zl/miniforge3/envs/coppeliasim/lib/python3.10/site-packages"
pip install osqp==0.6.2.post8 --force-reinstall
python -c "import osqp; print(f'OSQP version: {osqp.__version__}')"

View File

@ -12,18 +12,41 @@ import time
from math import radians, degrees, pi, cos, sin from math import radians, degrees, pi, cos, sin
import numpy as np import numpy as np
# pose expression of tool-tip in end-effector, x y z quatx quaty quatz quatw
# load: kg, mass_center_x in ee frame: m, y, z, then last threes are for filling
tools_in_ee = {
'scissor': np.array([[0.0, 0.0, 0.19, 0.0, 0.0, 0.0, 1.0],[0.66, 0.0, 0.0, 0.06, 0.0, 0.0, 0.0]],dtype=np.float64),
'omnipic': np.array([[0.0, 0.0, 0.16, 0.0, 0.0, 0.0, 1.0],[0.43, 0.0, 0.0, 0.06, 0.0, 0.0, 0.0]],dtype=np.float64),
'minisci': np.array([[0.0, 0.0, 0.19, 0.0, 0.0, 0.0, 1.0],[0.46, 0.0, 0.0, 0.06, 0.0, 0.0, 0.0]],dtype=np.float64),
'no_tool': np.array([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]],dtype=np.float64),
}
# joint limit
ub = np.array([150.0, 110.0, 170.0, 130, 175.0, 125.0, 179.0]) / 180 * pi
lb = np.array([-150.0, -30.0, -170.0, -130, -175.0, -125.0, -179.0]) / 180 * pi
# ub = np.array([179.0, 129.0, 179.0, 134, 179.0, 127.0, 359.0])/180*pi
# lb = -ub
tool_name = "scissor"
def main(): def main():
"""Demonstrate pure position control""" """Demonstrate pure position control"""
# Create controller # Create controller
robot_mjk = MuJoCoPositionController() robot_mjk = MuJoCoPositionController()
tool_name = "scissor"
# ----------- rm75 qp based kine ------------ # ----------- rm75 qp based kine ------------
robot_kine_qp = kine_qp() robot_kine_qp = kine_qp(urdf_path='/home/zl/Downloads/urdf_rm75/RM75-B.urdf', mesh_dir='/home/zl/Downloads/urdf_rm75')
robot_kine_qp.add_tool_frames(tools_in_ee)
robot_kine_qp.cfg_j_limit(min_j=lb, max_j=ub, rad_flag=True)
# ---------- rm75 official algorithm ----------- # ---------- rm75 official algorithm -----------
robot_kine_rm = kine_rm() robot_kine_rm = kine_rm()
robot_kine_rm.add_tool_frames(tools_in_ee)
robot_kine_rm.cfg_j_limit(min_j=lb, max_j=ub, rad_flag=True)
@ -32,16 +55,6 @@ def main():
if True: if True:
# ub = np.array([150.0, 110.0, 170.0, 130, 175.0, 125.0, 179.0])
# lb = np.array([-150.0, -30.0, -170.0, -130, -175.0, -125.0, -179.0])
ub = np.array([179.0, 129.0, 179.0, 134, 179.0, 127.0, 359.0])/180*pi
lb = -ub
robot_kine_qp.cfg_j_limit(min_j=lb, max_j=ub, rad_flag=True)
robot_kine_rm.cfg_j_limit(min_j=lb, max_j=ub, rad_flag=True)
result = np.array([[0,0],[0,0]], dtype=np.int32) # to collect ik result qp_fk, qp_ik, rm_fk, rm_ik result = np.array([[0,0],[0,0]], dtype=np.int32) # to collect ik result qp_fk, qp_ik, rm_fk, rm_ik
solve_sum = 0 solve_sum = 0
@ -95,7 +108,7 @@ def main():
if ret_qp == 0 or ret_rm == 0: if ret_qp == 0 or ret_rm == 0:
solve_sum += 1 solve_sum += 1
print(f'result is {result}') print(f'results with qp and rm for ik are {result}')
print(f'solve_sum is {solve_sum}') print(f'solve_sum is {solve_sum}')

View File

@ -8,6 +8,7 @@ import osqp
from scipy import sparse from scipy import sparse
from math import radians, degrees, pi, cos, sin from math import radians, degrees, pi, cos, sin
import time import time
import threading
@ -21,77 +22,19 @@ class KinematicsSolver():
print(f' ------------ the qp based kinematic initialising -----------') print(f' ------------ the qp based kinematic initialising -----------')
self.model, collision_model, visual_model = pin.buildModelsFromUrdf(urdf_path, mesh_dir) self.model, collision_model, visual_model = pin.buildModelsFromUrdf(urdf_path, mesh_dir)
# -------------------------------------------------
# ee
# -------------------------------------------------
ee_offset = pin.SE3(np.eye(3), np.array([0, 0, 0.0]))
self.model.addFrame(
pin.Frame(
"ee",
self.model.getJointId("joint_7"),
self.model.getFrameId("link_7"),
ee_offset,
pin.FrameType.OP_FRAME
)
)
# -------------------------------------------------
# Scissor tool
# -------------------------------------------------
scissor_offset = pin.SE3(
np.eye(3),
np.array([0.0, 0.0, 0.144])
)
self.model.addFrame(
pin.Frame(
"scissor",
self.model.getJointId("joint_7"),
self.model.getFrameId("link_7"),
scissor_offset,
pin.FrameType.OP_FRAME
)
)
# -------------------------------------------------
# Camera tool
# -------------------------------------------------
camera_rotation = pin.rpy.rpyToMatrix(
radians(-90),
0,
radians(-90)
)
camera_offset = pin.SE3(
camera_rotation,
np.array([0.05, 0.02, 0.10])
)
self.model.addFrame(
pin.Frame(
"camera",
self.model.getJointId("joint_7"),
self.model.getFrameId("link_7"),
camera_offset,
pin.FrameType.OP_FRAME
)
)
# -------------------------------------------------
# Store tool frame IDs
# -------------------------------------------------
self.tool_frames = {
"scissor": self.model.getFrameId("scissor"),
"camera": self.model.getFrameId("camera"),
"ee": self.model.getFrameId("ee")
}
self.data = self.model.createData()
self.cfg_j_limit() self.cfg_j_limit()
q_range = (
self.model.upperPositionLimit[:7] -
self.model.lowerPositionLimit[:7]
)
self.w_q_limit = np.diag(1.0 / (q_range ** 2))
self.q_mid = 0.5 * (self.model.lowerPositionLimit[:7] + self.model.upperPositionLimit[:7])
# ---------- for reused qp_solver ------------------ # ---------- for reused qp_solver ------------------
self.nv = 7 self.nv = 7
@ -117,6 +60,36 @@ class KinematicsSolver():
) )
self.W = np.diag([1, 1, 1, 0.4, 0.4, 0.4]) self.W = np.diag([1, 1, 1, 0.4, 0.4, 0.4])
# Smaller value => joint moves more actively
# Larger value => joint moves less / more lazy
self.joint_motion_weight = np.diag([
1.0, 1.0, 1.0, 1.0,
0.3, 0.3, 0.2
])
def add_frame(self,frame_name, position, rotationXYZ):
'''
:param frame_name: str
:param position: [x, y, z] target position (meters)
:param rotationXYZ: [x, y, z] target rotation (rad)
'''
camera_rotation = pin.rpy.rpyToMatrix( rotationXYZ[0], rotationXYZ[1], rotationXYZ[2] )
camera_offset = pin.SE3(
camera_rotation,
np.array(position)
)
self.model.addFrame( pin.Frame( frame_name, self.model.getJointId("joint_7"), self.model.getFrameId("link_7"), camera_offset, pin.FrameType.OP_FRAME ) )
def add_tool_frames(self,dict_frames):
self.tool_frames ={}
for tool_name in dict_frames:
tool_attr = dict_frames[tool_name]
position = tool_attr[0][0:3]
rotationXYZ = self.quaternion_to_euler(tool_attr[0][3:7])
self.add_frame(tool_name, position, rotationXYZ)
self.tool_frames.update({tool_name: self.model.getFrameId(tool_name)})
self.data = self.model.createData()
def cfg_j_limit(self, min_j=None, max_j=None, rad_flag = True): def cfg_j_limit(self, min_j=None, max_j=None, rad_flag = True):
if min_j is None: if min_j is None:
@ -132,7 +105,7 @@ class KinematicsSolver():
self.model.lowerPositionLimit[i] = min_j[i] / 180 * pi self.model.lowerPositionLimit[i] = min_j[i] / 180 * pi
self.model.upperPositionLimit[i] = max_j[i] / 180 * pi self.model.upperPositionLimit[i] = max_j[i] / 180 * pi
def forward_kinematics(self, joint_angles, tool="ee"): def forward_kinematics(self, joint_angles, tool="omnipic"):
""" """
Compute forward kinematics. Compute forward kinematics.
@ -201,8 +174,7 @@ class KinematicsSolver():
""" """
# Build target SE3 placement # Build target SE3 placement
if target_quat is not None: if target_quat is not None:
quat = pin.Quaternion(target_quat[3], target_quat[0], quat = pin.Quaternion(target_quat[3], target_quat[0], target_quat[1], target_quat[2])
target_quat[1], target_quat[2])
target_rotation = quat.matrix() target_rotation = quat.matrix()
elif target_rpy is not None: elif target_rpy is not None:
target_rotation = pin.rpy.rpyToMatrix(target_rpy[0], target_rotation = pin.rpy.rpyToMatrix(target_rpy[0],
@ -298,28 +270,28 @@ class KinematicsSolver():
# ========================= # =========================
# QP-based IK # QP-based IK
# ========================= # =========================
w_posture = 0.0001 w_ref = 0.0001
w_limit_mid = 0.00002
J_eff = pin.Jlog6(error_SE3) @ J #J # J_eff = pin.Jlog6(error_SE3) @ J #J #
H = J_eff.T @ self.W @ J_eff H = J_eff.T @ self.W @ J_eff
# H = J.T @ self.W @ J H += damping * damping * self.joint_motion_weight
H += damping * damping * np.eye(7) H += w_ref * np.eye(7)
H += w_posture * np.eye(7) H += w_limit_mid * self.w_q_limit
H_triu = sparse.triu(H).tocsc() H_triu = sparse.triu(H).tocsc()
g = -J_eff.T @ self.W @ error_vec g = -J_eff.T @ self.W @ error_vec
g += w_posture * (q[:7] - q_ref[:7]) g += w_ref * (q[:7] - q_ref[:7])
# g = - J.T @ self.W @ error_vec g += w_limit_mid * self.w_q_limit @ (q[:7] - self.q_mid)
# ------------------------- # -------------------------
# Joint velocity constraints # Joint velocity constraints
# ------------------------- # -------------------------
dq_limit = np.array([ 0.05, 0.05, 0.05, 0.05, 0.08, 0.08, 0.10 ]) # rad per iteration
dq_limit = 0.05 # rad per iteration
lb = -dq_limit * np.ones(7) lb = -dq_limit * np.ones(7)
ub = dq_limit * np.ones(7) ub = dq_limit * np.ones(7)
@ -364,10 +336,39 @@ class KinematicsSolver():
if best_solution is not None: if best_solution is not None:
# return best_solution, True, best_error, iter_count # return best_solution, True, best_error, iter_count
return 0, best_solution return 0, best_solution.tolist()
else: else:
# return q[:7].copy(), False, error_norm, iter_count # return q[:7].copy(), False, error_norm, iter_count
return -1, q[:7].copy() return -1, q[:7].copy().tolist()
def quaternion_to_euler(self, q):
"""
Convert quaternion to Euler angles (roll, pitch, yaw)
Args:
qx, qy, qz, qw: quaternion components
Returns:
tuple: (roll, pitch, yaw) in radians
"""
# Roll (x-axis rotation)
sinr_cosp = 2.0 * (q[3] * q[0] + q[1] * q[2])
cosr_cosp = 1.0 - 2.0 * (q[0] * q[0] + q[1] * q[1])
roll = np.arctan2(sinr_cosp, cosr_cosp)
# Pitch (y-axis rotation)
sinp = 2.0 * (q[3] * q[1] - q[2] * q[0])
if abs(sinp) >= 1:
pitch = np.copysign(np.pi / 2, sinp) # Use 90 degrees if out of range
else:
pitch = np.arcsin(sinp)
# Yaw (z-axis rotation)
siny_cosp = 2.0 * (q[3] * q[2] + q[0] * q[1])
cosy_cosp = 1.0 - 2.0 * (q[1] * q[1] + q[2] * q[2])
yaw = np.arctan2(siny_cosp, cosy_cosp)
return [roll, pitch, yaw]
# def invese_kinematics_velocity(self, target_position, target_rpy=None, # def invese_kinematics_velocity(self, target_position, target_rpy=None,
# target_quat=None, initial_guess=None, tool="ee"): # target_quat=None, initial_guess=None, tool="ee"):

View File

@ -14,16 +14,11 @@ class rm75_kine_api():
self.cfg_j_limit() self.cfg_j_limit()
self.tool_frames = {
'ee': rm_frame_t(frame_name="ee", pose=(0.0, 0.0, 0.0, 0.0, 0, 0.0), payload=1, x=0, y=0, z=0),
'scissor': rm_frame_t(frame_name="scissor", pose=(0.0, 0.0, 0.144, 0.0, 0, 0.0), payload=1, x=0, y=0, z=72),
'camera': rm_frame_t(frame_name="camera", pose=(0.05, 0.02, 0.10, -1.57, 0, -1.57), payload=1, x=0, y=0, z=72)
}
self.work_frames = { self.work_frames = {
'work': rm_frame_t(frame_name="ee", pose=(0.0, 0.0, 0.0, 0.0, 0, 0.0), payload=1, x=0, y=0, z=0), 'work': rm_frame_t(frame_name="work", pose=(0.0, 0.0, 0.0, 0.0, 0, 0.0), payload=1, x=0, y=0, z=0),
} }
self.tool_name = "ee" self.tool_name = "no_tool"
self.work_name = "work" self.work_name = "work"
def cfg_j_limit(self, min_j=None, max_j=None, rad_flag = True): def cfg_j_limit(self, min_j=None, max_j=None, rad_flag = True):
@ -32,6 +27,8 @@ class rm75_kine_api():
if min_j is None: if min_j is None:
min_j = np.array([ -3.14159, -2.2689, -3.14159, -2.3562, -3.14159, -2.234, -3.14159 ]) min_j = np.array([ -3.14159, -2.2689, -3.14159, -2.3562, -3.14159, -2.234, -3.14159 ])
max_j = np.array(max_j)
min_j = np.array(min_j)
if rad_flag: if rad_flag:
self.robot_kine_rm.rm_algo_set_joint_max_limit((max_j * 180 / math.pi).tolist()) self.robot_kine_rm.rm_algo_set_joint_max_limit((max_j * 180 / math.pi).tolist())
self.robot_kine_rm.rm_algo_set_joint_min_limit((min_j * 180 / math.pi).tolist()) self.robot_kine_rm.rm_algo_set_joint_min_limit((min_j * 180 / math.pi).tolist())
@ -51,7 +48,46 @@ class rm75_kine_api():
def get_tool_frame(self): def get_tool_frame(self):
return self.robot_kine_rm.rm_algo_get_curr_toolframe() return self.robot_kine_rm.rm_algo_get_curr_toolframe()
def forward_kinematics(self, joint_angles, flag = 1 , tool="ee", work="work"): def quaternion_to_euler(self, q):
"""
Convert quaternion to Euler angles (roll, pitch, yaw)
Args:
qx, qy, qz, qw: quaternion components
Returns:
tuple: (roll, pitch, yaw) in radians
"""
# Roll (x-axis rotation)
sinr_cosp = 2.0 * (q[3] * q[0] + q[1] * q[2])
cosr_cosp = 1.0 - 2.0 * (q[0] * q[0] + q[1] * q[1])
roll = np.arctan2(sinr_cosp, cosr_cosp)
# Pitch (y-axis rotation)
sinp = 2.0 * (q[3] * q[1] - q[2] * q[0])
if abs(sinp) >= 1:
pitch = np.copysign(np.pi / 2, sinp) # Use 90 degrees if out of range
else:
pitch = np.arcsin(sinp)
# Yaw (z-axis rotation)
siny_cosp = 2.0 * (q[3] * q[2] + q[0] * q[1])
cosy_cosp = 1.0 - 2.0 * (q[1] * q[1] + q[2] * q[2])
yaw = np.arctan2(siny_cosp, cosy_cosp)
return [roll, pitch, yaw]
def add_tool_frames(self, dict_frames):
self.tool_frames = {}
for tool_name in dict_frames:
tool_attr = dict_frames[tool_name]
position = tool_attr[0][0:3]
rotationXYZ = self.quaternion_to_euler(tool_attr[0][3:7])
f = rm_frame_t(frame_name=tool_name, pose=(position[0], position[1], position[2], rotationXYZ[0], rotationXYZ[1], rotationXYZ[2]), payload=1, x=0, y=0, z=0)
self.tool_frames.update({tool_name:f})
def forward_kinematics(self, joint_angles, flag = 1 , tool="omnipic", work="work"):
''' '''
:param joint_angles: list of joint values, in rad :param joint_angles: list of joint values, in rad
:param flag: 0: return list [x,y,z,w,x,y,z]. 1: return list [x,y,z,rx,ry,rz] :param flag: 0: return list [x,y,z,w,x,y,z]. 1: return list [x,y,z,rx,ry,rz]
@ -66,7 +102,7 @@ class rm75_kine_api():
return self.robot_kine_rm.rm_algo_forward_kinematics(joint=[q_s*180/math.pi for q_s in joint_angles] , flag=flag) return self.robot_kine_rm.rm_algo_forward_kinematics(joint=[q_s*180/math.pi for q_s in joint_angles] , flag=flag)
def inverse_kinematics(self, target_position, target_rpy=None, initial_guess=None, tool="ee", work="work"): def inverse_kinematics(self, target_position, target_rpy=None, initial_guess=None, tool="omnipic", work="work"):
''' '''
:param target_position: list of position values, m :param target_position: list of position values, m
:param target_rpy: list of rpy values, rad :param target_rpy: list of rpy values, rad