Ros2_shadow: running a candidate node next to production and diffing what they'd do

I kept running into the same thing. You’ve got a planner that works, you write a new one, and the only way to really know if it’s better is real data. But real data means the real robot, which means handing control to something you don’t trust yet. So the new version sits around untested, or you convince yourself sim was good enough.

So I wrote ros2_shadow. Both nodes run at once on the same inputs, only production actually controls anything, and the tool sits in the middle logging where the two would have done something different.

Output looks like this:

ros2_shadow  /shadow/planner/path vs /planner/path
matched 27   unmatched prod 0   unmatched cand 0   pending 0
paired on header.stamp

metric                    mean       p95       max
endpoint_distance       0.2663    0.3900    0.3900
hausdorff_distance      1.7351    4.7955    4.7955
length_difference       0.4506    1.1071    1.1071

warnings 14   critical 13

On keeping the candidate away from hardware. My first instinct was namespace remapping plus a denylist of topics it isn’t allowed to publish on. That doesn’t actually work. Remapping only applies to names the node declares through the API, so anything built at runtime slips through, and services and actions aren’t covered at all. And a graph watcher only tells you about a breach after the message has gone out, which is too late to be worth much.

So the candidate gets its own ROS_DOMAIN_ID instead. The hardware topics simply don’t exist over there. A bridge process holds two rclpy contexts and moves only the topics you list, and it prints exactly what crosses before it starts. I tested it with a candidate hammering /cmd_vel at 20 Hz: 0 messages made it into the production domain, 161 in its own.

Worth being clear that this only covers ROS-level access. If your candidate opens a serial port, you want a container.

The matching turned out to be the actual hard part. Both nodes see the same input but finish at different times, so you can’t just compare the latest message from each. I buffer both sides and pair within a tolerance.

The annoying bit is which timestamp to pair on. header.stamp is the right answer when it’s there, but plenty of nodes don’t copy the input’s stamp onto the output. Nav2’s planner stamps its path with now(), so the field you want to join on just isn’t populated with anything useful. Falls back to arrival time in that case, and the tool tells you which clock it used, because a pairing based on arrival is a much weaker claim than one based on the stamp of the input that produced both.

Metrics are per message type. A generic diff isn’t much use:

Type Metrics
Twist linear/angular error, direction_reversal
PoseStamped translation, rotation error
JointTrajectory per-joint RMSE, max delta, endpoint, duration
Path Hausdorff, mean deviation, endpoint, length difference

direction_reversal is the one I’d point at. If production says +0.4 and the candidate says -0.3, an error norm calls that 0.7 and buries it next to ordinary drift. It’s not drift, the robot is going backwards.

Paths get several numbers for a similar reason. Big Hausdorff with a small endpoint distance means it went a different way to the same place, which is usually fine. Small Hausdorff with a big endpoint distance means it followed you and then stopped somewhere else, which usually isn’t. One number can’t tell you which happened.

Demo is two Nav2 planner servers on one map with the same costmap config, NavFn as production and Smac 2D as the candidate, same goals to both. No simulator, the probe just hands them start poses.

ros2 launch ros2_shadow nav2_shadow_demo.launch.py
ros2 run ros2_shadow shadow config/nav2_shadow.yaml
rviz2 -d rviz/nav2_shadow.rviz

They end up routing about 4.8 m apart while finishing within 0.39 m of each other, which is the “different route, same destination” case above.

demo

Github

Mostly I’d like to know which message types people would need that aren’t there, and whether the domain isolation approach survives contact with how you actually deploy.

3 Likes

Hey - neat project!

That generally shouldn’t be true, as long as you don’t add a / in your topic strings in the code. Namespacing the node should actually work for this case otherwise. If using a Nav2 server, you can be sure that namespacing works properly as we do so for our cloned/isolated multi-robot configurations and I’ve validated the Planner Server recently with a new replay testing framework for global planning.

Though obviously changing the domain ID is the most isolated it can be (however makes publishing the output to visualize in rviz difficult to visually compare).

If your intention is just a A-B visual comparison, I think this or a more simplified version is a pretty neat idea. If you’d like to generalize this to more offline testing for validation of performance changes, algorithm differences, or tuning impacts - that’s what replay testing is more suited for.

1 Like

Ah, good to know. I’ll walk that back then. I tried it with a node that had / hardcoded and assumed it was a general issue, but it sounds like it isn’t.

The domain ID issue also broke the RViz side for me, which is part of why I’ve already removed the bridge and just pointed people to ros2/domain_bridge.

Is the replay framework public anywhere? Honestly, the only part I’m really attached to is the metrics. Hausdorff distance plus endpoint distance tells you whether it took a different path to the same destination or simply stopped short.

You lose that distinction with a single metric. If those metrics make more sense as part of the replay setup, I’d rather contribute them there than keep a separate tool running.

Not quite yet, but it is in its final testing stages so definitely before EOY but with ROSCon coming up, probably sometime in October.

The replay framework has the ability to inject your own metrics for gating / baseline comparison - but I do actually include a path comparison metric (but if you don’t like the one I implemented, its easy to add your own as a plugin).

Something I haven’t put much thought / work into yet on the replay framework is visualization and interactive editing/adding test cases. If that interests you for your own needs, lets chat separately! Also happy to have your contributions anywhere in Nav2 as well :slight_smile:

Sorry, missed this until now!! :slightly_frowning_face:

Visualization and test case editing are the parts I’d be most interested in, and I’ve got a few ideas from building similar stuff. Happy to chat whenever suits, or wait until it’s public if that’s easier.