[{"data":1,"prerenderedAt":644},["ShallowReactive",2],{"i-mdi:white-balance-sunny":3,"i-mdi:close":8,"i-mdi:menu":10,"blog":12,"i-mdi:arrow-top-right":642},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"currentColor\" d=\"m3.55 19.09l1.41 1.41l1.8-1.79l-1.42-1.42M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6s6-2.69 6-6c0-3.32-2.69-6-6-6m8 7h3v-2h-3m-2.76 7.71l1.8 1.79l1.41-1.41l-1.79-1.8M20.45 5l-1.41-1.4l-1.8 1.79l1.42 1.42M13 1h-2v3h2M6.76 5.39L4.96 3.6L3.55 5l1.79 1.81zM1 13h3v-2H1m12 9h-2v3h2\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"currentColor\" d=\"M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z\"\u002F>",[13],{"id":14,"title":15,"body":16,"date":632,"description":633,"extension":634,"links":635,"meta":636,"navigation":637,"path":638,"preview":24,"seo":639,"slug":635,"stem":640,"tags":635,"__hash__":641},"blog\u002Fblog\u002Fimitation_learning_pipeline.md","Imitation Learning Pipeline",{"type":17,"value":18,"toc":612},"minimark",[19,29,32,37,40,43,87,91,94,110,115,122,137,141,148,180,202,206,215,222,228,240,244,366,382,388,392,409,420,428,437,444,448,451,455,461,464,483,489,505,511,514,520,523,533,537,540,605,609],[20,21,25],"prose-img",{"className":22,"src":24},[23],"w-full","\u002Fimages\u002Fmotoman_cheezit.webp",[26,27,28],"p",{},"Motoman SDA10F robot picking up a Cheezit box in a MuJoCo simulation.",[26,30,31],{},"During my research assistantship at the Pracsys Lab during the summer of 2025, there was growing interest in imitation learning and large-scale VLA models. However, our lab lacked the infrastructure to collect data and deploy these policies on our robots. I volunteered to build this pipeline so we could conduct experiments with the new VLA models.",[33,34,36],"h2",{"id":35},"goals","Goals",[26,38,39],{},"Our existing robot software stack is built around our Motoman SDA10F robot and is designed for open-loop control. However, it was placed in a Catkin workspace with several unused packages, files, and scripts — making it difficult to understand. It also ran on Ubuntu 20.04 and ROS Noetic — both older software versions. We also had access to another lab's UR5e robot, but it had never been used before.",[26,41,42],{},"Our goals for the new infrastructure were as follows:",[44,45,46,62,72],"ol",{},[47,48,49,53],"li",{},[50,51,52],"strong",{},"Clarity",[54,55,56,59],"ul",{},[47,57,58],{},"The ROS workspace should have a minimal set of packages and code. Additional unused packages add mental load to learning how the project works.",[47,60,61],{},"Code should be self-documenting or contain comments to explain specific implementation quirks.",[47,63,64,67],{},[50,65,66],{},"Flexibility",[54,68,69],{},[47,70,71],{},"Standard APIs should be used whenever possible to avoid locking code into a specific robot, software library, or implementation.",[47,73,74,77],{},[50,75,76],{},"Reuse",[54,78,79],{},[47,80,81,82],{},"The workspace should run on the same Ubuntu and ROS versions as the old code to reuse existing packages whenever possible.\n",[54,83,84],{},[47,85,86],{},"Since the SDA10F teleoperation code depends on now-deprecated libraries, I wanted to avoid upgrading ROS because that would entail re-implementing these libraries. Reusing existing code that is proven to work would therefore minimize the amount of time taken to implement a feature.",[33,88,90],{"id":89},"ros-design","ROS Design",[26,92,93],{},"With these goals in mind, I designed and implemented the following ROS node infrastructure:",[20,95,99],{"className":96,"src":98},[97],"bg-white","\u002Fsvgs\u002Fimitation_learning_ros.svg",[26,100,101,102,109],{},"ROS node architecture. Dotted boxes ",[103,104,108],"a",{"href":105,"rel":106},"https:\u002F\u002Fgoogle.com",[107],"nofollow","represent"," a common interface\u002FAPI.",[111,112,114],"h3",{"id":113},"real-nodes","Real Nodes",[26,116,117,118,121],{},"Every piece of hardware in ROS — from ZED Mini cameras to the SDA10F — comes with its own ROS node that handles the low-level driver communication required to fetch images from a camera or control a joint on a robot. The \"API\" of a ROS node can be seen as the topics and services that it publishes. These ",[119,120,114],"code",{}," aren't standardized and expose a variety of different APIs.",[123,124,127,130],"admonition",{"title":125,"type":126},"Example","info",[26,128,129],{},"Since the Motoman SDA10F is a dual-arm robot, its underlying driver treats each arm as a separate \"robot group.\" And despite each arm sharing the same base joint, the base joint itself for each arm is treated as a separate \"robot group.\"",[26,131,132,133,136],{},"Therefore, the Motoman ROS node publishes to ",[50,134,135],{},"four"," joint-state topics.",[111,138,140],{"id":139},"robot-interface","Robot Interface",[26,142,143,144,147],{},"While we could modify the source code of the real nodes to standardize their API, that would make the library codebase more fragile to upstream changes from the original authors. Therefore, ",[119,145,146],{},"Robot Interface Nodes"," were created to convert their non-standard ROS node APIs to a standard API.",[54,149,150,169],{},[47,151,152,153],{},"All robots\n",[54,154,155,162],{},[47,156,157,158,161],{},"Published their current joint states to a ",[119,159,160],{},"\u002FROBOT_NAME\u002Fjoint_states"," topic",[47,163,164,165,168],{},"Listened to ",[119,166,167],{},"\u002Fteleop\u002Fjoint_states"," to control their joint positions",[47,170,171,172,175,176,179],{},"All cameras published their RGB and depth images to ",[119,173,174],{},"\u002FCAMERA_NAME\u002Frgb"," and ",[119,177,178],{},"\u002FCAMERA_NAME\u002Fdepth"," topics",[123,181,182,193,196,199],{"title":125,"type":126},[26,183,184,185,188,189,192],{},"These interfaces sometimes have to do non-trivial conversions, such as in the case of the ",[119,186,187],{},"Motoman Interface Node",". Due to how the underlying Motoman driver works, the ",[119,190,191],{},"Real Motoman Node"," has a joint streaming quirk where any joint command sent during streaming cannot be interrupted.",[26,194,195],{},"Joint commands are also published with a duration that the movement should take, and the underlying driver then moves to the new joint states over the specified duration. If joint commands are sent faster than the Motoman can consume them, it can create a backlog that can eventually overflow the Motoman driver's internal queue.",[26,197,198],{},"Therefore, the interface node has to publish joint commands at the rate the driver is following them, by checking if the current joint state is within a specific tolerance of the target joint state.",[26,200,201],{},"This is just one example of a Motoman driver quirk. Small hardware-specific behaviors like these required additional code to coerce into our standardized API.",[111,203,205],{"id":204},"robot-sim","Robot Sim",[26,207,208,209,214],{},"To support collecting data and deploying policies in simulation, I used the open-source ",[103,210,213],{"href":211,"rel":212},"https:\u002F\u002Fgithub.com\u002Fgoogle-deepmind\u002Fmujoco",[107],"MuJoCo"," physics engine. Since we had existing code and digital models built for the SDA10F in MuJoCo, this was the easiest simulator to use.",[26,216,217,218,221],{},"I created a ",[119,219,220],{},"MuJoCo Sim Node"," that simulates a scene and exposes a ROS API for interacting with it. It publishes topics for joint states and camera images while listening to topics for joint control.",[26,223,224,225,227],{},"Then, simulated versions of the hardware nodes were created that exposed the same API as their underlying hardware but used the MuJoCo Sim under the hood. By replicating the existing hardware API, I could test ",[119,226,140],{}," nodes in simulation prior to deploying them on the real robot.",[123,229,230],{"title":125,"type":126},[26,231,232,233,236,237,239],{},"The ",[119,234,235],{},"Motoman Sim Node"," would expose the four joint-state topics that the real Motoman node used, as well as the ROS services for controlling joint streaming. This joint streaming followed the same non-interruptible and duration-driven behavior as the real robot, and this replica was especially useful in testing our ",[119,238,187],{}," implementation.",[111,241,243],{"id":242},"teleoperation","Teleoperation",[26,245,246,247,250,251,253,254,256,257,365],{},"To publish the joint commands for the robot, I built ",[119,248,249],{},"Teleop Nodes",". These nodes publish to ",[119,252,167],{},", which is the same topic that ",[119,255,146],{}," consume. Teleop nodes can represent human input (keyboard, GELLO, etc.) or a deployed policy running inference (",[258,259,262,292],"span",{"className":260},[261],"katex",[258,263,266],{"className":264},[265],"katex-mathml",[267,268,270],"math",{"xmlns":269},"http:\u002F\u002Fwww.w3.org\u002F1998\u002FMath\u002FMathML",[271,272,273,287],"semantics",{},[274,275,276],"mrow",{},[277,278,279,283],"msub",{},[280,281,282],"mi",{},"π",[284,285,286],"mn",{},"0",[288,289,291],"annotation",{"encoding":290},"application\u002Fx-tex","\\pi_0",[258,293,297],{"className":294,"ariaHidden":296},[295],"katex-html","true",[258,298,301,306],{"className":299},[300],"base",[258,302],{"className":303,"style":305},[304],"strut","height:0.5806em;vertical-align:-0.15em;",[258,307,310,315],{"className":308},[309],"mord",[258,311,282],{"className":312,"style":314},[309,313],"mathnormal","margin-right:0.0359em;",[258,316,319],{"className":317},[318],"msupsub",[258,320,324,356],{"className":321},[322,323],"vlist-t","vlist-t2",[258,325,328,351],{"className":326},[327],"vlist-r",[258,329,333],{"className":330,"style":332},[331],"vlist","height:0.3011em;",[258,334,336,341],{"style":335},"top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;",[258,337],{"className":338,"style":340},[339],"pstrut","height:2.7em;",[258,342,348],{"className":343},[344,345,346,347],"sizing","reset-size6","size3","mtight",[258,349,286],{"className":350},[309,347],[258,352,355],{"className":353},[354],"vlist-s","​",[258,357,359],{"className":358},[327],[258,360,363],{"className":361,"style":362},[331],"height:0.15em;",[258,364],{},", etc).",[367,368,373],"prose-video",{":controls":296,":loop":296,":muted":296,"loading":369,"src":370,"className":371},"lazy","\u002Fvideos\u002Fgello.mp4",[372],"h-[32rem]",[26,374,375,376,381],{},"Teleoperation on a real Ur5e robot using a 3D-printed ",[103,377,380],{"href":378,"rel":379},"https:\u002F\u002Fwuphilipp.github.io\u002Fgello_site\u002F",[107],"GELLO"," controller.",[367,383,385],{":controls":296,":loop":296,":muted":296,"loading":369,"src":384},"\u002Fvideos\u002Fkeyboard_teleop.mp4",[26,386,387],{},"Teleoperation on a simulated Motoman SDA10F robot using a keyboard + mouse.",[111,389,391],{"id":390},"data-collection","Data Collection",[26,393,394,395,398,399,402,403,408],{},"Finally, a ",[119,396,397],{},"Data Collector Node"," collects all the information — robot joint states, camera images, and teleop joint commands — and sends it to a ",[119,400,401],{},"Data Writer"," process, which writes the data to disk in a specific format (",[103,404,407],{"href":405,"rel":406},"https:\u002F\u002Fgithub.com\u002Fhuggingface\u002Flerobot\u002F",[107],"LeRobot",", HDF5, etc.). Each run or \"episode\" is a single action, such as stacking a pile of cubes or closing a drawer.",[123,410,412,417],{"title":411,"type":126},"Note",[26,413,232,414,416],{},[119,415,401],{}," is not a ROS node but rather a separate Python process that uses ZMQ sockets. Due to using Ubuntu 20.04 and ROS Noetic, we are forced to use Python 3.8 for ROS. However, many Python libraries only work with newer Python versions.",[26,418,419],{},"Therefore, I moved the data writing code to a separate process to support data writers that use different Python versions.",[367,421,425],{":controls":296,":loop":296,":muted":296,"loading":369,"src":422,"className":423},"\u002Fvideos\u002Fcube_stack.webm",[424],"h-auto",[26,426,427],{},"Data collection in simulation using a GELLO arm.",[26,429,430,431,436],{},"To better visualize our data, I wrote a script that can load an episode into ",[103,432,435],{"href":433,"rel":434},"https:\u002F\u002Fgithub.com\u002Frerun-io\u002Frerun",[107],"rerun.io",". These replays can help us diagnose what happened during a run — especially when we're collecting data from several experiments in simulation.",[367,438,441],{":controls":296,":loop":296,":muted":296,"loading":369,"src":439,"className":440},"\u002Fvideos\u002Frerun.webm",[424],[26,442,443],{},"Reviewing collected data inside rerun.",[33,445,447],{"id":446},"additional-bits","Additional Bits",[26,449,450],{},"Aside from the core ROS architecture, these are the most important miscellaneous scripts I wrote for the project.",[111,452,454],{"id":453},"robot-config","Robot Config",[26,456,232,457,460],{},[119,458,459],{},"robot_config.py"," script holds a dictionary of different robot configurations. Each robot configuration includes joint, MuJoCo, gripper, and camera information.",[26,462,463],{},"Each ROS node that needs robot-specific information can then load a configuration from a name passed into its CLI.",[123,465,466,473],{"title":125,"type":126},[26,467,468,469,472],{},"A keyboard_teleop node that wants to load the ",[119,470,471],{},"motoman"," config and control its first arm would run the following:",[474,475,480],"pre",{"className":476,"code":478,"language":479},[477],"language-text","keyboard_teleop.py motoman 0\n","text",[119,481,478],{"__ignoreMap":482},"",[111,484,486],{"id":485},"run_rospy",[119,487,488],{},"run_ros.py",[26,490,491,492,495,496,501,502,504],{},"This script reads a ",[119,493,494],{},".yaml"," ROS config file that specifies a list of commands to run, and then executes the commands in a single ",[103,497,500],{"href":498,"rel":499},"https:\u002F\u002Fgithub.com\u002Ftmux\u002Ftmux\u002Fwiki",[107],"tmux"," session with different tmux windows. The ",[119,503,494],{}," config file can also define variables that are substituted into the commands for more dynamic behaviors.",[111,506,508],{"id":507},"ws_installsh",[119,509,510],{},"ws_install.sh",[26,512,513],{},"This script installs the entire workspace onto a new Ubuntu 20.04 computer. It automatically installs required dependencies if they aren't present.",[111,515,517],{"id":516},"hotreloadpy",[119,518,519],{},"hotreload.py",[26,521,522],{},"This script opens a MuJoCo scene XML file in a MuJoCo viewer and hot-reloads the viewer whenever the scene XML is changed.",[367,524,527],{":controls":296,":loop":296,":muted":296,"loading":369,"src":525,"className":526},"\u002Fvideos\u002Fhotreload.mp4",[424],[26,528,529,530,532],{},"Editing a scene with ",[119,531,519],{},".",[33,534,536],{"id":535},"lessons","Lessons",[26,538,539],{},"After finishing this project, I had the following takeaways:",[44,541,542,563,589],{},[47,543,544,547],{},[50,545,546],{},"Understand Your Hardware — Document Everything!",[54,548,549,552],{},[47,550,551],{},"There will inevitably come a time when the robot throws an error on its pendant and it's up to you to trace down a fix. Especially for older\u002Fniche robots (like the Motoman SDA10F), online discourse and troubleshooting resources are limited.",[47,553,554,555,558,559,562],{},"After encountering several unique errors on the Motoman that took days to troubleshoot, I created a ",[119,556,557],{},"motoman\u002Fdocumentation"," folder in the repository that stores manuals for the Motoman SDA10F robot, the FS100 controller, the Robotiq 2F-85 gripper, and the Robotiq universal controller. Additionally, I wrote a ",[119,560,561],{},"README.md"," file that documents the basic operation of the robot, manual control through the pendant, robot joint calibration, and steps for resolving specific errors that may occur on the pendant. By documenting your hardware, the next grad student who inherits the robot will have an easier time working with it.",[47,564,565,568],{},[50,566,567],{},"Quality of Life Matters",[54,569,570,573,579,584],{},[47,571,572],{},"Small scripts can make a big difference in maintaining a cleaner workspace and working with our physics simulator.",[47,574,575,576,578],{},"Keeping our ",[119,577,510],{}," script updated not only makes it easy to rebuild the repository if something breaks, but also makes it easy to onboard new undergraduates onto the repository.",[47,580,232,581,583],{},[119,582,519],{}," script brought a \"web-dev-like\" experience of live editing to MuJoCo, making it faster to build scenes by hand.",[47,585,232,586,588],{},[119,587,488],{}," script created a standard way to define ROS node networks, allowing us to build different configs for simulation and real environments.",[47,590,591,594],{},[50,592,593],{},"Simplify, Simplify",[54,595,596,599],{},[47,597,598],{},"While ROS nodes serve as a useful framework for splitting code, too many nodes can slow down performance and hog CPU time due to the overhead of forwarding messages and running multiple processes.",[47,600,601,602,604],{},"My choice to replicate an existing hardware API in sim — while making it easier to test the interface classes — added additional complexity and computational overhead. A simpler architecture would have the simulation nodes follow the standard ",[119,603,140],{}," API directly.",[33,606,608],{"id":607},"closing","Closing",[26,610,611],{},"This project was my first shot at building a ROS infrastructure from scratch. Coming in with no background in robotics, I quickly learned new software like ROS, RViz, and MuJoCo, and gained experience in designing modular ROS nodes. I'm looking forward to applying these skills to my next project.",{"title":482,"searchDepth":613,"depth":613,"links":614},2,[615,616,624,630,631],{"id":35,"depth":613,"text":36},{"id":89,"depth":613,"text":90,"children":617},[618,620,621,622,623],{"id":113,"depth":619,"text":114},3,{"id":139,"depth":619,"text":140},{"id":204,"depth":619,"text":205},{"id":242,"depth":619,"text":243},{"id":390,"depth":619,"text":391},{"id":446,"depth":613,"text":447,"children":625},[626,627,628,629],{"id":453,"depth":619,"text":454},{"id":485,"depth":619,"text":488},{"id":507,"depth":619,"text":510},{"id":516,"depth":619,"text":519},{"id":535,"depth":613,"text":536},{"id":607,"depth":613,"text":608},"8\u002F31\u002F2025","Summary of the software engineering behind building an \nimitation learning infrastructure for the Pracsys Lab.\n","md",null,{},true,"\u002Fblog\u002Fimitation_learning_pipeline",{"title":15,"description":633},"blog\u002Fimitation_learning_pipeline","s5d5q9Zi0Ecd_3vjebeFYUAVfYB8KH6iuwf_WlZ9enQ",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":643},"\u003Cpath fill=\"currentColor\" d=\"M5 17.59L15.59 7H9V5h10v10h-2V8.41L6.41 19z\"\u002F>",1783983664038]