Control
Use Joint Tracker during runtime by loading the identified joint-controller model bundle, computing an optimized joint command, and sending that command to the robot manufacturer's SDK.
Before using this page, complete Calibration and Identification so that src/robot/models/current/joint_tracker contains the identified Joint Tracker model bundle.
Inputs
The Joint Tracker controller needs:
- The controller sample time, in seconds.
- The identified model folder, usually
src/robot/models/current/joint_tracker. - The desired joint position trajectory in radians.
- The number of robot joints and model-backed axes.
The robot SDK is still responsible for executing the optimized command on the robot.
Example 1: Full Known Trajectory
Use this example when the full joint trajectory is known before the move starts. This example is ideal for short and repeatable trajectories.
Python SDK
pythonimport numpy as np import robot_sdk # Replace with the SDK package for the robot you are using. from reforge_core.control.joint_tracker import ( JointTrackerConfig, JointTrackerInterface, JointTrackerOptimizerOptions, ) sample_time_s = 0.004 model_directory = "src/robot/models/current/joint_tracker" num_joints = 6 # Initialize Joint Tracker with the identified model bundle. config = JointTrackerConfig( num_joints=num_joints, model_directory=model_directory, modeled_axis_indices=list(range(num_joints)), sample_time_s=sample_time_s, optimizer_options=JointTrackerOptimizerOptions(lam_u=0.01), ) initializing_JTC = JointTrackerInterface(config) # Replace this line with the desired joint trajectory from your planner or robot SDK. desired_trajectory_rad = np.asarray(robot_sdk.get_planned_joint_trajectory()) # Optimize the complete desired trajectory before sending it to the robot. JTC_offline_mode = initializing_JTC.process_trajectory( command=desired_trajectory_rad, ) # Send the optimized command with your robot SDK. robot_sdk.send_joint_trajectory( positions=JTC_offline_mode.positions, velocities=JTC_offline_mode.velocities, accelerations=JTC_offline_mode.accelerations, sample_time=sample_time_s, )
C++ SDK
cpp#include <cstddef> #include <string> #include <vector> #include "reforge_core/joint_tracker/joint_tracker.hpp" namespace jt = reforge::joint_tracker; int main() { const double sample_time_s = 0.004; const std::string model_directory = "src/robot/models/current/joint_tracker"; const std::size_t num_joints = 6; // Initialize Joint Tracker with the identified model bundle. jt::JointTrackerConfig config; config.sample_time_s = sample_time_s; config.num_joints = num_joints; config.modeled_axis_indices = {0, 1, 2, 3, 4, 5}; config.optimizer_options.lam_u = 0.01; jt::JointTracker initializing_JTC(config); initializing_JTC.LoadModels(model_directory); // Replace this line with the desired joint trajectory from your planner or robot SDK. const std::vector<std::vector<double>> desired_trajectory_rad = get_planned_joint_trajectory_rad(); const jt::JointTrajectory desired_trajectory = jt::JointTrajectory::FromPositions( desired_trajectory_rad, sample_time_s); // Optimize the complete desired trajectory before sending it to the robot. const jt::JointTrajectory JTC_offline_mode = initializing_JTC.OptimizeTrajectory(desired_trajectory); // Send the optimized command with your robot SDK. send_joint_trajectory( JTC_offline_mode.samples(), sample_time_s); }
Example 2A: Stream a Known Full Trajectory
Use this example when the full trajectory is known before execution, but you want Joint Tracker to optimize and emit command windows while the robot is already moving. This way, you do not need to wait until the whole trajectory is optimized. This example is ideal for applications where the trajectory is known beforehand but is very long.
Example 2A keeps the default controller settings and is the known-stream parity example: its optimized command should match Example 1's offline output.
Python SDK
pythonimport numpy as np import robot_sdk # Replace with the SDK package for the robot you are using. from reforge_core.control.joint_tracker import JointTrackerConfig, JointTrackerInterface sample_time_s = 0.004 model_directory = "src/robot/models/current/joint_tracker" num_joints = 6 # Initialize Joint Tracker with the identified model bundle. config = JointTrackerConfig( num_joints=num_joints, model_directory=model_directory, modeled_axis_indices=list(range(num_joints)), sample_time_s=sample_time_s, ) initializing_JTC = JointTrackerInterface(config) # Replace this line with the full desired joint trajectory from your planner. desired_trajectory_rad = np.asarray(robot_sdk.get_planned_joint_trajectory()) # Create the stream-mode controller and append the complete known trajectory. JTC_stream_mode = initializing_JTC.process_trajectory_stream_mode() JTC_stream_mode.append_reference(desired_trajectory_rad) # Tell Joint Tracker that no more desired samples will be appended. JTC_stream_mode.finish() # Start optimizing command windows in the background. JTC_stream_mode.start() # Consume optimized command windows and send each window to the robot SDK. while not JTC_stream_mode.is_complete(): command_window = JTC_stream_mode.pop() if command_window is None: break robot_sdk.send_joint_trajectory( positions=command_window.positions, velocities=command_window.velocities, accelerations=command_window.accelerations, sample_time=sample_time_s, ) JTC_stream_mode.close()
C++ SDK
cpp#include <cstddef> #include <optional> #include <string> #include <vector> #include "reforge_core/joint_tracker/joint_tracker.hpp" namespace jt = reforge::joint_tracker; int main() { const double sample_time_s = 0.004; const std::string model_directory = "src/robot/models/current/joint_tracker"; const std::size_t num_joints = 6; // Initialize Joint Tracker with the identified model bundle. jt::JointTrackerConfig config; config.sample_time_s = sample_time_s; config.num_joints = num_joints; config.modeled_axis_indices = {0, 1, 2, 3, 4, 5}; jt::JointTracker initializing_JTC(config); initializing_JTC.LoadModels(model_directory); // Replace this line with the full desired joint trajectory from your planner. const std::vector<std::vector<double>> desired_trajectory_rad = get_planned_joint_trajectory_rad(); const jt::JointTrajectory desired_trajectory = jt::JointTrajectory::FromPositions( desired_trajectory_rad, sample_time_s); // Create the stream-mode controller and append the complete known trajectory. jt::JointTrackerStream JTC_stream_mode = initializing_JTC.CreateStream(); JTC_stream_mode.AppendReference(desired_trajectory); // Tell Joint Tracker that no more desired samples will be appended. JTC_stream_mode.Finish(); // Start optimizing command windows. C++ streams emit windows synchronously // from PopWindow(...), so no background start() call is required. // Consume optimized command windows and send each window to the robot SDK. while (true) { std::optional<jt::JointTrackerWindow> command_window = JTC_stream_mode.PopWindow(); if (!command_window.has_value()) { break; } send_joint_trajectory( command_window->trajectory.samples(), sample_time_s); } }
Example 2B: Stream an Online Trajectory
Use this example when your trajectory is generated online while the robot is moving. Example 2B is intended for planners that append one desired sample at a time and need Joint Tracker to make one optimized sample available to send to the robot.
Python SDK
pythonimport numpy as np import robot_sdk # Replace with the SDK package for the robot you are using. from reforge_core.control.joint_tracker import ( JointTrackerConfig, JointTrackerInterface, JointTrackerOptimizerOptions, ) sample_time_s = 0.004 model_directory = "src/robot/models/current/joint_tracker" num_joints = 6 SINGLE_SAMPLE_N_APPLY_SCALE = 1.0e-9 # Initialize Joint Tracker for one-sample online streaming. n_apply_scale tunes # how many optimized samples Joint Tracker makes available per window. Smaller # positive values make shorter windows; this example uses one-sample windows. config = JointTrackerConfig( num_joints=num_joints, model_directory=model_directory, modeled_axis_indices=list(range(num_joints)), sample_time_s=sample_time_s, optimizer_options=JointTrackerOptimizerOptions( n_apply_scale=SINGLE_SAMPLE_N_APPLY_SCALE, ), ) initializing_JTC = JointTrackerInterface(config) # Create the stream-mode controller. JTC_online_mode = initializing_JTC.process_trajectory_stream_mode() # Append exactly one first desired sample before starting the stream. Each # planner update below also appends exactly one sample. desired_trajectory_beginning_rad = np.asarray( robot_sdk.get_next_desired_joint_sample() ).reshape(1, num_joints) JTC_online_mode.append_reference(desired_trajectory_beginning_rad) # Start optimizing command windows in the background. JTC_online_mode.start() # Real-time loop: each iteration feeds JTC the planner's latest samples and # sends one optimized command window to the robot. It exits once JTC has # optimized and drained every sample you appended. while not JTC_online_mode.is_complete(): # YOUR PLANNER: append exactly one new desired sample for each planner # update. When it has nothing new (e.g. a pause between moves), skip the # append and JTC holds the last desired sample until the planner recovers. if robot_sdk.has_new_desired_joint_samples(): new_desired_samples_rad = np.asarray( robot_sdk.get_next_desired_joint_sample() ).reshape(1, num_joints) JTC_online_mode.append_reference(new_desired_samples_rad) # YOUR PLANNER: once no more samples will ever be appended, tell JTC to wind # down so the loop can finish. finish() does not stop the JTC but it let it # know that your path planner will no longer generate new samples. if robot_sdk.planner_is_finished(): JTC_online_mode.finish() # JTC: take the next optimized command window. pop() blocks until a window # is ready and returns None only after the stream is finished and drained. command_window = JTC_online_mode.pop() if command_window is None: break # YOUR ROBOT SDK: send the optimized command window to the robot. robot_sdk.send_joint_trajectory( positions=command_window.positions, velocities=command_window.velocities, accelerations=command_window.accelerations, sample_time=sample_time_s, ) # JTC: release the background optimizer thread when you are done. JTC_online_mode.close()
C++ SDK
cpp#include <cstddef> #include <optional> #include <string> #include <vector> #include "reforge_core/joint_tracker/joint_tracker.hpp" namespace jt = reforge::joint_tracker; int main() { const double sample_time_s = 0.004; const std::string model_directory = "src/robot/models/current/joint_tracker"; const std::size_t num_joints = 6; constexpr double kSingleSampleNApplyScale = 1.0e-9; // Initialize Joint Tracker for one-sample online streaming. n_apply_scale // tunes how many optimized samples Joint Tracker makes available per window. // Smaller positive values make shorter windows; this example uses // one-sample windows. jt::JointTrackerConfig config; config.sample_time_s = sample_time_s; config.num_joints = num_joints; config.modeled_axis_indices = {0, 1, 2, 3, 4, 5}; config.optimizer_options.n_apply_scale = kSingleSampleNApplyScale; jt::JointTracker initializing_JTC(config); initializing_JTC.LoadModels(model_directory); // Create the stream-mode controller. jt::JointTrackerStream JTC_online_mode = initializing_JTC.CreateStream(); // Append exactly one first desired sample before starting the stream. Each // planner update below also appends exactly one sample. const std::vector<std::vector<double>> desired_trajectory_beginning_rad = { get_next_desired_joint_sample_rad()}; JTC_online_mode.AppendReference( jt::JointTrajectory::FromPositions( desired_trajectory_beginning_rad, sample_time_s)); // Start optimizing command windows. C++ streams emit windows synchronously // from PopWindow(...), so no background start() call is required. // Real-time loop: each iteration feeds JTC the planner's latest samples and // sends one optimized command window to the robot. It exits once JTC has // optimized and drained every sample you appended. while (true) { // YOUR PLANNER: append exactly one new desired sample for each planner // update. When it has nothing new (e.g. a pause between moves), skip the // append and JTC holds the last desired sample until the planner recovers. if (has_new_desired_joint_samples()) { const std::vector<std::vector<double>> new_desired_samples_rad = { get_next_desired_joint_sample_rad()}; JTC_online_mode.AppendReference( jt::JointTrajectory::FromPositions( new_desired_samples_rad, sample_time_s)); } // YOUR PLANNER: once no more samples will ever be appended, tell JTC to wind // down so the loop can finish. Finish() does not stop the JTC but it let it // know that your path planner will no longer generate new samples. if (planner_is_finished()) { JTC_online_mode.Finish(); } // JTC: take the next optimized command window. PopWindow() returns an // empty optional only after the stream has no window ready. std::optional<jt::JointTrackerWindow> command_window = JTC_online_mode.PopWindow(); if (!command_window.has_value()) { if (JTC_online_mode.finished()) { break; } continue; } // YOUR ROBOT SDK: send the optimized command window to the robot. send_joint_trajectory( command_window->trajectory.samples(), sample_time_s); } }
Runnable Repository Example
The minimal snippets above show only the controller usage. Full hardware-free simulation examples, including trajectory generation, plotting, diagnostics, and expected results, are available in:
bashsrc/robot/example_usage/joint_tracker/python/joint_tracker_example_usage.py src/robot/example_usage/joint_tracker/cpp/main.cpp
Run the Python example from the repository root with:
bashpython3 src/robot/example_usage/joint_tracker/python/joint_tracker_example_usage.py
Build and run the C++ example from its folder with:
bashsudo apt install reforge-core-joint-tracker cmake g++ python3 python3-matplotlib cd src/robot/example_usage/joint_tracker/cpp cmake -S . -B build cmake --build build --parallel ./build/joint_tracker_example_usage
The C++ example links the public ReforgeJointTracker::joint_tracker target from the installed reforge-core-joint-tracker package plus its local plotting adapter. Use the package release that corresponds to this documentation; Example 2B requires a package that provides JointTrackerConfig::optimizer_options.
Expected Output
Running the example prints RMS tracking-error summaries to the console and renders two figures.
Figure 1 — Robot commands
![]()
This figure shows the joint commands sent to the robot. It is laid out as a grid: one column per joint, and three rows — position [rad], velocity [rad/s], and acceleration [rad/s²]. Three commands are overlaid on every subplot:
- Desired command (black, solid): the raw reference trajectory you want the robot to follow.
- Example 1 command (blue, solid): the command JTC optimizes from the fully known trajectory (Example 1 / stream mode).
- Example 2A command (purple, dashed): the command JTC optimizes from the known trajectory while the robot loop consumes streamed windows.
The key takeaway is that the optimized commands deliberately differ from the desired command — JTC pre-shapes the position, velocity, and acceleration profiles to counteract each joint's flexible dynamics. The Example 1 and Example 2A commands overlap almost exactly, confirming that the known-trajectory streamed path reproduces the offline result.
Figure 2 — Robot response
![]()
This figure shows the resulting robot motion — the simulated plant response to each command — with one subplot per joint (position [rad] versus time):
- Desired trajectory (black, dotted): the reference the joint should track.
- Response to desired command (gray): how the joint actually moves when the raw desired command is sent directly. Note the overshoot and residual oscillation, most visible after the motion ends.
- Response to JTC Example 1 command (blue) and Response to JTC Example 2A command (purple, dashed): how the joint moves when driven by the JTC-optimized commands. Both track the desired trajectory closely.
- The red dotted vertical line marks the end of the commanded motion;
Together the figures tell the whole story: JTC changes the command (Figure 1) so that the robot's actual response (Figure 2) follows the desired trajectory with far less tracking error than commanding the desired trajectory directly. The RMS tracking-error values printed to the console quantify that improvement.