8000 Created kinematics module exploiting DH frames: forward/inverse kinematics, Jacobians, pose interpolator by leo-berte · Pull Request #1001 · huggingface/lerobot · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Created kinematics module exploiting DH frames: forward/inverse kinematics, Jacobians, pose interpolator #1001

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 48 commits into from

Conversation

leo-berte
Copy link
Contributor
@leo-berte leo-berte commented Apr 19, 2025

What this does

This module is designed to compute both forward and inverse kinematics accurately, and it is easily extensible to robots with more degrees of freedom.

  • Forward kinematics exploiting DH tables
  • Jacobian computation exploiting DH tables.
  • Inverse kinematics using Jacobian transpose and dump-least square method to avoid singularities.
  • Pose interpolation: linear (position) + SLERP (orientation)

@AdilZouitine: we spoke during Mistral AI Robotics Hackaton about a potential upgrade in the kinematics module.
@Cadene

Title Label
Closes issue #[678] Missing Inverse Kinematics Module
Adds new methods exploiting DH tables forward/inverse kinematics, Jacobians, pose interpolator

Closes

Closes #678

Notes

Suggested labels: enhancement, feature request

How it was tested

In "main" inside kinematics.py I have performed the following testing example:

  • Initializes the "so100" robot model.
  • Define a goal pose worldTtool.
  • Solves IK with only position tracking.
  • Prints joint angles and corresponding final pose with direct kinematics.
  • Checked goal pose worldTtool and corresponding final pose with direct kinematics are the same.
  • It prints the final Cartesian error norm between these two poses.

How to checkout & try?

  • Run kinematics.py and check the prints in the shell.
python lerobot/scripts/common/kinematics/kinematics.py
  • LeRobot Team shall only compute the correct DH table for the real robot (e.g. SO100) and insert them in the code. For now I have used only fake numbers for testing. More info on how to setup DH table in the attached PDF file or README.md

Check the following files in the folder:

leo-berte and others added 5 commits April 19, 2025 12:25
Added README file explaining kinematics module.
Added kinematics module, PDF exaplining DH frames and image
Edited README
Edited README
$$

### Note
The only remaining step is to **compute the corresponding DH table** (see image) given the real robots. Since I do not own SO100/koch/moss robots, I hope LeRobot Team can measure the corresponding parameters and provide the DH table. Happy to anwer any further question, I also attach the PDF "robot kinematics" for more details about DH tables.
Copy link
@alexis779 alexis779 Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For SO100, those measurements should be available in the CAD files in the STEP folder:
https://github.com/TheRobotStudio/SO-ARM100/tree/main/STEP

It will be more accurate than manually measuring the distances on the physical 3D-printed files.

Also would a simulator supporting URDF / MJCF format help to derive the DH table parameters ?

  • mujoco
  • maniskill
  • genesis
  • etc.

Here's the Mujoco config: https://github.com/google-deepmind/mujoco_menagerie/tree/main/trs_so_arm100/
Here's the URDF config: https://github.com/TheRobotStudio/SO-ARM100/blob/main/URDF/SO_5DOF_ARM100_8j_URDF.SLDASM/urdf/SO_5DOF_ARM100_8j_URDF.SLDASM.urdf

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the resources, I'll have a look over the next days and I'll be back!

Copy link
Contributor Author
@leo-berte leo-berte Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @alexis779 and @imstevenpmwork ! Here my updates:

  • I computed the SO100 Denavit-Hartenberg frames and table using precise measurements from the CAD model.

  • Updated the code with the corrected SO100 DH table.

  • Correctly tested the forward/inverse kinematics with the new DH convention.

  • Updated the README with images showing the DH frames and the DH table for SO100.

Note: the angles used in the code are DH angles, not direct motor angles — they depend on how the robot is assembled. As a final step, we could either update the assembly tutorial to match the DH conventions or explore methods to implement motors2DH and DH2motors conversions.

That said, the updated DH table can already be tested on the real robot! Please refer to the updated README.md for the correct angle conventions.

Let me know if you have any feedback — happy to chat about how to tackle that final step!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a SO100 and I can test your script. I'll comment with the results of my QA.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leo-berte

I'm interested in evaluating the torque that's just necessary to counterbalance the natural forces on the joints. Mainly the contributions from

  1. gravitational force applied by the forward links, for each joint
  2. Coulomb friction from the motor gears

I have read the gravitational torque can also be computed through the analytical jacobian, using the transpose of that matrix. I'll also need the Center Of Mass + mass of the higher links ...

Let me know if you have some thoughts on this new application of the jacobian, how to measure the parameters and derive the inputs

Copy link
@alexis779 alexis779 Apr 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before testing on real robot, there's 1 feature missing, the ability to enforce qpos lower and upper limit.

Would your current approach relying on Damped Least Squared (DLS) be extensible to handle those additionnal constraints?

Compare DLS vs OMPL.

DLS

  • DLS violates constraints, causing the simulator to enter impossible positions
  • It also requires a well-calibrated damping factor, here damping = 6e-4

MotionTrackingPseudoInverseSoARM100DLQ.mp4

OMPL

OMPL version will make sure to rotate the shoulder pan back to the other side when hitting the limit.

MotionTrackingPseudoInverseSoARM100OMPLLimit.mp4

Limits

lower = [-2.2000, -3.1416,  0.0000, -2.0000, -3.1416, -0.2000]
upper = [2.2000, 0.2000, 3.1416, 1.8000, 3.1416, 2.0000]

Constraint Programming

A possible solution is to formulate the problem for a Quadratic Solver, minimizing the squared error with linear constraints on the variables

Minimize ∥JΔq−Δx∥^2+λ∥Δq∥^2
 
Subject to 
q_min ≤ q + Δq ≤ q_max

Script

To reproduce the simulation, apply this diff
diffik_controller.txt
to the Differential Inverse Kinematics example script.

Copy link
Contributor Author
@leo-berte leo-berte Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leo-berte

I'm interested in evaluating the torque that's just necessary to counterbalance the natural forces on the joints. Mainly the contributions from

  1. gravitational force applied by the forward links, for each joint
  2. Coulomb friction from the motor gears

I have read the gravitational torque can also be computed through the analytical jacobian, using the transpose of that matrix. I'll also need the Center Of Mass + mass of the higher links ...

Let me know if you have some thoughts on this new application of the jacobian, how to measure the parameters and derive the inputs

That's a really interesting problem! I've seen gravity compensation used in robots where you have access to low-level motor control and can send a feed-forward compensation term directly to the joint controller. Do we have that level of access on the SO100 motors? Or are you thinking of using this for other purposes?

For the compensation algorithm, you can definitely build on the DH frames. You’d need to express the center of mass and inertia tensor of each link with respect to its associated DH frame. Then, using the Newton-Euler recursive algorithm, you can propagate external forces and moments backward from the end-effector to compute joint torques due to gravity.

To estimate the mass and CoM of each link, you can use SolidWorks or any CAD tool. Just set the material (e.g. PLA for 3D printed parts). That should give reasonably accurate results for the structural parts. However, estimating contribution of the motors (inertia tensor) might be not precise to estimate from CAD.

As for Coulomb friction, the typical model is:

tau_friction = tau_coulumb + D*W

At equilibrium: tau_friction = Ki = tau_coulumb + DW

You can identify the parameters tau_coulumb and D via system identification: inject known currents, measure joint velocities W and fit the model.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before testing on real robot, there's 1 feature missing, the ability to enforce qpos lower and upper limit.

Would your current approach relying on Damped Least Squared (DLS) be extensible to handle those additionnal constraints?

Compare DLS vs OMPL.

DLS

  • DLS violates constraints, causing the simulator to enter impossible positions
  • It also requires a well-calibrated damping factor, here damping = 6e-4

MotionTrackingPseudoInverseSoARM100DLQ.mp4

OMPL

OMPL version will make sure to rotate the shoulder pan back to the other side when hitting the limit.

MotionTrackingPseudoInverseSoARM100OMPLLimit.mp4

Limits

lower = [-2.2000, -3.1416,  0.0000, -2.0000, -3.1416, -0.2000]
upper = [2.2000, 0.2000, 3.1416, 1.8000, 3.1416, 2.0000]

Constraint Programming

A possible solution is to formulate the problem for a Quadratic Solver, minimizing the squared error with linear constraints on the variables

Minimize ∥JΔq−Δx∥^2+λ∥Δq∥^2
 
Subject to 
q_min ≤ q + Δq ≤ q_max

Script

To reproduce the simulation, apply this diff diffik_controller.txt to the Differential Inverse Kinematics example script.

DLS does not take into account joint limits directly. However, as mentioned below, I just inserted a method check_joint_limits() to be called which raises an error in case of out of bound vales and thus provide safe deploy on real robot.

As future next steps, for sure we can explore more advanced techniques like weighted DLS (basically I use a weight matrix which penalizes usage of joints near limits) or optimization formulations.

@imstevenpmwork imstevenpmwork added enhancement Suggestions for new features or improvements robots Issues concerning robots HW interfaces labels Apr 23, 2025
print("Final joint angles = ", q_final)
print("Final pose direct kinematics = \n", T_final)

print("err_lin = ", RobotUtils.calc_lin_err(T_goal, T_final))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also add a unit test that evaluates the delta between the original goal and the final state.

Assert that the error in euclidean norm is below 0.01 for example ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I updated the inverse_kinematics method by adding an assert statement in case the error norm is greater than 0.01.

Then, at raw 306 in the main I added a new non-reachable T_goal and if you run the code you see it gives you an AssertionError.

Just pushed the code ;)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran the script and got an assertion error.

Traceback (most recent call last):
  File "/home/alexis/Documents/python/robotics/slobot/scripts/kinematics.py", line 47, in <module>
    q_final = kin.inverse_kinematics(robot, q_init, T_goal, use_orientation=False, k=0.8, n_iter=50)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alexis/Documents/python/robotics/slobot/.venv/lib/python3.12/site-packages/lerobot/common/kinematics/kinematics.py", line 230, in inverse_kinematics
    assert lin_error_norm < 1e-2, (
           ^^^^^^^^^^^^^^^^^^^^^

AssertionError: [ERROR] Large position error (0.0672). Check target reachability (position/orientation)

7% error sounds a bit high, any clue ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch Alexis! However, here my goal was to trigger this type of error, so I set T_goal as a far position unreachable from the robot (even if full extended). So the high error is due to this reason.

I just pushed new code with unit testing in a seperate file (unit_tests.py). At row 30, you see I set T_goal even further (I add 1m along x-axis, so you notice even better it will not be reachable), thus triggering that error.

So I think everything behaves as expected about this issue ;)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be included in this PR?

Pointing to a public download url should be sufficient.

To generate it, drag and drop your file into a Github comment. It will automatically upload it to the cloud.

Like this: robot kinematics.pdf

This online course is also a good resource: https://www.edx.org/learn/robotics/universita-degli-studi-di-napoli-federico-ii-robotics-foundations-i-robot-modeling

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I removed the file from the folder and inserted it as a comment in this PR thread!

print("Final joint angles = ", q_final)
print("Final pose direct kinematics = \n", T_final)

print("err_lin = ", RobotUtils.calc_lin_err(T_goal, T_final))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran the script and got an assertion error.

Traceback (most recent call last):
  File "/home/alexis/Documents/python/robotics/slobot/scripts/kinematics.py", line 47, in <module>
    q_final = kin.inverse_kinematics(robot, q_init, T_goal, use_orientation=False, k=0.8, n_iter=50)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alexis/Documents/python/robotics/slobot/.venv/lib/python3.12/site-packages/lerobot/common/kinematics/kinematics.py", line 230, in inverse_kinematics
    assert lin_error_norm < 1e-2, (
           ^^^^^^^^^^^^^^^^^^^^^

AssertionError: [ERROR] Large position error (0.0672). Check target reachability (position/orientation)

7% error sounds a bit high, any clue ?

]
}

FROM_DH_TO_MECH = {"so100": [90.0, 90.0, 90.0, 90.0, 90.0]}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SO-ARM-100 DOF is 6, so there should be 6 values in this array.

The 6th is for the mobile jaw. It is fine to ignore it for the purpose of forward kinematics.

Is the assumption that the end effector is the link connected to joint 5, the fixed jaw.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I just pushed a chage to handle the fact that q_init is a 6DOF vector including also the gripper!

So far, there is the assumption that the robot n-frame is exactly at the tip of the gripper, so forward/inverse kinematics behave accordingly (see SO100 DH frames image for references).

I have also introduced in the code nTtool, which someone can set and that could handle automatically the fact that maybe someone added a different gripper. However, as mentioned at row = 42 in kinematics.py, nTtool can be used only when use_orientation = True in the inverse_kinematics().

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename the joints, Joint 1, Joint 2, ... to lerobot motor names
["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper"]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sure the current SO100 has its own frames, used also in simulation. However, I do not see a 1 to 1 correspondence with DH frames (someone has different orientation/origin).

So maybe we could just leave the kinematics.py module with DH frames as designed in dh1.png, and make sure people can use it just my completing the dh2mech() and mech2dh functions (which depends on SO100 assembly).

By doing so, maybe, at the end, also for the simulator can be done a mech2dh / dh2mech trick, by pretending that current simulator motor commands are mechanical quantities to be converted in DH quantities.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diagram is very helpful.

should we also add joint 6 for the mobile jaw in the left diagram ?

SoARM100LinkFrames.mp4

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other comment on DH frames

@leo-berte
Copy link
Contributor Author

Upload useful resource for DH convention: robot kinematics.pdf

@leo-berte
Copy link
Contributor Author

Thanks Alexis for the ee_start and ee_goal orientations! I was able to do more testing, and I just pushed:

  • Function to convert mechanical2DH
  • Function to convert DH2mechanical
  • Added worldTbase (basically the world frame is the base frame of the robot in the simulator, so now global poses in sim are aligned with my code)
  • Added nTtool (basically the tool frame is the n-frame of the robot in the simulator, so now global poses in sim are aligned with my code)
  • Updated dh1.png image to show world frame and tool frame in blue

Could you try this motion? Is the same you gave me, but it moves downwords so that the robot can keep the same orientation, while moving upwards the robot is not able to maintain it due to unreachable positions. If you run my kinematics.py, then you could compare it with the simulator.

qpos_start = [-np.pi/2, -np.pi/2, np.pi/2, np.pi/2, -np.pi/2, np.pi/2]
T_start = forward_kinematics(qpos_start)
T_goal = T_start.copy()
T_goal[:3, 3] += np.array([0.0, 0.0, -0.1])

Last point: I am not sure about the conversion dh2mech and mech2dh for joint 1 and joint 5 (see lines 72 and 76). This is because I do not have enough data to compute the conversion function. So, could you please call the forward kinematics from the simulator on these configurations?

qpos_start = [0, -np.pi/2, np.pi/2, np.pi/2, -np.pi/2, np.pi/2] # just joint 1 has changed
qpos_start = [-np.pi, -np.pi/2, np.pi/2, np.pi/2, -np.pi/2, np.pi/2] # just joint 1 has changed

qpos_start = [-np.pi/2, -np.pi/2, np.pi/2, np.pi/2, 0, np.pi/2] # just joint 5 has changed
qpos_start = [-np.pi/2, -np.pi/2, np.pi/2, np.pi/2, -np.pi, np.pi/2] # just joint 5 has changed

If you could paste here the position + orientation for these 4 configs would be really helpful!

Thank you, let me know!

@alexis779
Copy link

qpos_start = [0, -np.pi/2, np.pi/2, np.pi/2, -np.pi/2, np.pi/2] # just joint 1 has changed
qpos_start = [-np.pi, -np.pi/2, np.pi/2, np.pi/2, -np.pi/2, np.pi/2] # just joint 1 has changed

qpos_start = [-np.pi/2, -np.pi/2, np.pi/2, np.pi/2, 0, np.pi/2] # just joint 5 has changed
qpos_start = [-np.pi/2, -np.pi/2, np.pi/2, np.pi/2, -np.pi, np.pi/2] # just joint 5 has changed

qpos (radians) Position (tensor) Quaternion (tensor)
[0, -1.5708, 1.5708, 1.5708, -1.5708, 1.5708] [ 0.0000, -0.2387, 0.1767] [ 7.0710e-01, 7.0711e-01, -2.2352e-06, -2.2352e-06]
[-3.1416, -1.5708, 1.5708, 1.5708, -1.5708, 1.5708] [1.6916e-08, 1.4830e-01, 1.7667e-01] [-2.2352e-06, -2.2352e-06, -7.0711e-01, -7.0711e-01]
[-1.5708, -1.5708, 1.5708, 1.5708, 0, 1.5708] [-0.1935, -0.0452, 0.1767] [ 7.0711e-01, 7.0711e-01, -4.4703e-06, 0.0000e+00]
[-1.5708, -1.5708, 1.5708, 1.5708, -3.1416, 1.5708] [-0.1935, -0.0452, 0.1767] [-4.5013e-06, -3.0909e-08, -7.0711e-01, -7.0711e-01]

@leo-berte
Copy link
Contributor Author

Thanks Alexis, with your last inputs I could carry out the last tests.

I completed both the dh2mechanical and mechanical2dh methods to do these conversions, and I also added the related unit tests. Hopefully everything works now!

Could you try to replicate in the simulator the motion I do in my main() in kinematics.py please? Something similar to:

qpos_start = [-np.pi/2, -np.pi/2, np.pi/2, np.pi/2, -np.pi/2, np.pi/2]
T_start = forward_kinematics(qpos_start)
T_goal = T_start.copy()
T_goal[:3, 3] += np.array([0.0, 0.0, -0.1])
move_to(T_goal) # PSEUDOCODE

Basically, the Cartesian poses and the mechanical angles shall be aligned with the simulator now.

Looking forward to your feedback, in case everything works, it's finally time to test on the real robot!

@alexis779
Copy link
alexis779 commented May 20, 2025

Translation by dz=-0.1

This move is impossible: The tip of the fixed jaw hits the ground before the arm can lower the fixed jaw by 0.1 m vertically.

The z-location of the fixed jaw is where you see the horizontal red bar (not the green bar) coming out of the joint towards us.

LowerTouchTheGround

Translation by dz=+0.1

The solution to the IK problem should be

[-1.5708, -1.7186,  1.1014,  1.3481, -1.5708,  1.5708]

Do you think you can get a unit test to cover the lift by +0.1 m action ?

LiftPose

@leo-berte
Copy link
Contributor Author
leo-berte commented May 20, 2025

Yes apologies, I did not realize the robot would have touched the ground.

However, I did not use dz+=0.1 beacuse in the final pose the robot is not able anymore to keep the starting orientation (mechanically unfeasible).

So with:

T_goal[:3, 3] += np.array([0.0, 0.0, 0.1])
q_final_dh = kin.inverse_kinematics(robot, q_init_dh, T_goal, use_orientation=False, k=0.8, n_iter=50)

My code reaches the goal pose. but you need to set use_orientation=False. Since it does not track the orientation, our final joint angles may be different, but the direct kinematic shall be the same (position part only).

I would suggest using another test as well where the orientation is kept, so that we can compare also the orientation part. For example:

T_goal[:3, 3] += np.array([-0.05, 0.0, -0.05])
q_final_dh = kin.inverse_kinematics(robot, q_init_dh, T_goal, use_orientation=True, k=0.8, n_iter=50)

In general, if you can, I would suggest you trying some poses before in simulation (to see they are feasible and their orientation is conserved), and then try to command those poses on my code and check the final direct kinematics.

@alexis779
Copy link

I was able to translate the fixed jaw by -0.05, keeping the same orientation.

IKLeo

Compare the qpos vector from your version (L) and from Genesis (G)

L [-1.57079633, -1.49317879, 1.85288823, 1.21108721, -1.57079633, 1.57079633]
G [-1.5708,     -1.5100,     1.8744,     1.2065,     -1.5708,     1.5708]

They match !

Here's the full script. Please make sure i used your IK library the way you intended.

To make it pass, I had to update from_mech_to_dh to make it returns a vector of dimension = 5

@leo-berte
Copy link
Contributor Author
leo-berte commented May 21, 2025

Happy to see the results match!!

The logic behind your usage of my IK code is correct, but I noted you wrote on line 17:

# Remove mobile jaw variable
qpos_current = qpos_current[:-1]

However, this is the intended usage for those 2 methods:

  • from_mech_to_dh(): takes as input a 6dim vector (the method itself will then remove automatically the gripper coordinate)
  • from_dh_to_mech(): takes as input a 5dim vector

(See lines 320-361 in main() in kinematics.py if you need better understanding)

Let me know whether you think we can move to closing this PR ;)

@alexis779
Copy link
  • from_mech_to_dh(): takes as input a 6dim vector (the method itself will then remove automatically the gripper coordinate)

Ok that makes sense. I fixed my script accordingly.

Let me know whether you think we can move to closing this PR ;)

Yes, this PR looks good to me!

@leo-berte leo-berte closed this May 22, 2025
@leo-berte
Copy link
Contributor Author

Awesome, thanks Alexis! I closed the PR then, please let me know if everything went well or if I need to do somethig more (manually close related issue #678, delete kinematics_module branch, .)

@michaeljelly
Copy link

This is cool, how come you closed it instead of merging it @leo-berte @alexis779 ?

@soulninja-dev
Copy link
soulninja-dev 893F commented Jul 9, 2025

This is cool, how come you closed it instead of merging it @leo-berte @alexis779 ?

+1, why is this not merged?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Suggestions for new features or improvements robots Issues concerning robots HW interfaces
Projects
None yet
Development

Successfully merging this pull request may close these issues.

The inverse kinematic solution code of so-100
5 participants
0