[JTC] Global cubic spline upsampling for positions-only action chunks - #2443
[JTC] Global cubic spline upsampling for positions-only action chunks#2443vedh1234 wants to merge 12 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2443 +/- ##
==========================================
+ Coverage 86.82% 86.99% +0.16%
==========================================
Files 148 151 +3
Lines 16097 15336 -761
Branches 1361 1272 -89
==========================================
- Hits 13976 13341 -635
+ Misses 1615 1538 -77
+ Partials 506 457 -49
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
This pull request is in conflict. Could you fix it @vedh1234? |
saikishor
left a comment
There was a problem hiding this comment.
Good job. The code looks good. However, the change is minimal here, I'm not sure if we should somehow inject this into JTC already and not to make a variant out of it. As we have plans to extend this, we can leave it, but we should reevaluate this soon
| void InferenceBridgeController::topic_callback( | ||
| const std::shared_ptr<trajectory_msgs::msg::JointTrajectory> msg) | ||
| { | ||
| // TODO: add staleness / max-age check for stamp=0 chunks |
There was a problem hiding this comment.
Yes, we can do it, I'll look into it.
| trajectory_msgs::msg::JointTrajectory & traj) const | ||
| { | ||
| // Respect timing the policy already set; only synthesize when it is absent. | ||
| for (size_t i = 1; i < traj.points.size(); ++i) |
There was a problem hiding this comment.
time_from_start is measured from the start of the trajectory, point 0 (first waypoint) is always at t = 0 by definition. So no point in checking the first value as we would not know if user had added it or not.
| inference_bridge_controller: | ||
| policy_frequency: { | ||
| type: double, | ||
| default_value: 30.0, |
There was a problem hiding this comment.
I wouldn't put any default value, if it is already a different controller
There was a problem hiding this comment.
Okay, I'll keep it as a required field.
| } | ||
| } | ||
|
|
||
| void fill_cubic_spline_velocities(trajectory_msgs::msg::JointTrajectory & traj) |
There was a problem hiding this comment.
May be return bool as you are handling failure cases here
There was a problem hiding this comment.
Agreed, I'll make the required change.
| diagonal[i] = 2.0 * (1.0 / duration_prev + 1.0 / duration_next); | ||
| upper_diagonal[i] = 1.0 / duration_next; | ||
| rhs[i] = 3.0 * ((pos_next - pos_curr) / (duration_next * duration_next) + |
There was a problem hiding this comment.
What are these 2.0 and 3.0 here?
There was a problem hiding this comment.
They're the coefficients of the cubic-Hermite equation. They come from second derivative of the equations.
| TEST(TestInferenceBridgeController, preserves_header_stamp) | ||
| { | ||
| TestableBridge bridge; | ||
| auto traj = make_positions_chunk({0.0, 0.1, 0.2, 0.1, 0.0}, 0.05); | ||
| // Clear timing so synthesize_timing actually runs, but keep a non-zero stamp. | ||
| for (auto & point : traj.points) | ||
| { | ||
| point.time_from_start = rclcpp::Duration::from_seconds(0.0); | ||
| } | ||
| traj.header.stamp.sec = 123; | ||
| traj.header.stamp.nanosec = 456u; | ||
|
|
||
| bridge.synthesize_timing(traj); | ||
| joint_trajectory_controller::fill_cubic_spline_velocities(traj); |
There was a problem hiding this comment.
It would be nice to add a test of the generated velocities
There was a problem hiding this comment.
Okay I'll add a a dedicated velocity-value test.
Co-authored-by: Sai Kishor Kothakota <saisastra3@gmail.com>
Description
Add
InferenceBridgeController, a JTC extension that upsamples sparse, positions-only trajectory chunks into smooth global C2 cubic-spline trajectories.Background:
AI / learned policies (e.g. diffusion policy, ACT) emit action chunks: short
JointTrajectorymessages containing only positions at the policy's output rate (e.g. 10–50 Hz), with no velocities or accelerations. Fed positions-only, JTC interpolates linearly (C0) producing velocity discontinuities and jerk spikes at every knot. This is unsuitable for real hardware.Approach
The bridge intercepts incoming
~/joint_trajectorymessages via JTC'stopic_callbackand, for positions-only chunks:policy_frequencyparameter whentime_from_startis absent.fill_cubic_spline_velocities(), an O(n) tridiagonal Thomas solve with rest boundary conditions (v₀ = vₙ₋₁ = 0) and writes them intopoints[i].velocities.Chunks that already carry velocities pass through unchanged, the controller is a strict superset of JTC.
Is this user-facing behavior change?
Yes. Users can now load
joint_trajectory_controller/InferenceBridgeControlleras a drop-in replacement for JTC. Positions-only trajectory messages are automatically upsampled to smooth C2 cubic splines. The only new parameter ispolicy_frequency(Hz), which controls the synthesised timing for chunks that arrive withouttime_from_start. Existing trajectories with velocities are unaffected.Did you use Generative AI?
Yes, Google Gemini and Claude were used to make improvements in manually written code, architectural analysis, edge-case review. All generated code was reviewed, modified, and verified by the author.
Additional Information
Known limitations / future work:
~/joint_trajectory) is upsampled.FollowJointTrajectoryaction goals bypasstopic_callbackand are not upsampled.TODOs
To send us a pull request, please:
colcon testandpre-commit run(requires you to install pre-commit bypip3 install pre-commit)Test Scenario:
Two position only motion profiles were sent on the using the
~/joint_trajectory. As we can see,InferenceBridgeControllerbeing C2 continuous, the staircase like discontinuity is not observed in velocity plot in both motion profiles. The sharp corners we observe are due to the lack of cross chunk continuity.