Chapter 12: ROS/ROS2 Foundations (Practical Intro)
Lesson 6: Best Practices for Reproducible Robot Code
This lesson formalizes what it means for robot software to be reproducible, then gives concrete engineering patterns in ROS/ROS2 to achieve it. We treat reproducibility as a property of a computational experiment: the same code and inputs should yield the same observable robot behavior, within quantified tolerances. You will learn how to control randomness, time, concurrency, dependencies, and hardware/system variation using ROS tooling, containerization, and CI, with multi-language examples.
1. Formal Meaning of Reproducibility
In robotics, a “run” is an interaction between software, sensors, actuators, and the physical world. Let an experiment be a tuple \( \mathcal{E} = (C, I, \Omega, H, T) \) where:
- \( C \): code (including build configuration and parameters),
- \( I \): initial conditions (robot state, environment state),
- \( \Omega \): stochastic sources (random seeds, noise realizations),
- \( H \): hardware + OS + middleware stack,
- \( T \): timing model (clock source, rates, scheduling).
We can model the observed robot behavior as a (possibly stochastic) map \( \mathcal{B} = F(\mathcal{E}) \), where \( \mathcal{B} \) is the set of observable trajectories (topic time series, actuator commands, logs).
Strong reproducibility means: for two runs \( \mathcal{E}_1 \) and \( \mathcal{E}_2 \) with identical components, the behaviors match exactly:
\[ \mathcal{E}_1 = \mathcal{E}_2 \;\Rightarrow\; F(\mathcal{E}_1) = F(\mathcal{E}_2). \]
In physical robotics, exact equality can be too strict due to floating-point rounding, OS scheduling jitter, sensor quantization, and real-world variation. We therefore use practical reproducibility: there exists a tolerance \( \varepsilon \ge 0 \) such that behavior distance satisfies \( d(F(\mathcal{E}_1), F(\mathcal{E}_2)) \le \varepsilon \).
\[ \text{Practical reproducibility:}\quad \mathcal{E}_1 = \mathcal{E}_2 \;\Rightarrow\; d(\mathcal{B}_1,\mathcal{B}_2) \le \varepsilon. \]
A typical distance is time-aligned squared error between logged topic sequences. If \( y(t) \) is a scalar topic with sampling times \( t_k \), define:
\[ d(\mathcal{B}_1,\mathcal{B}_2) = \left( \sum_{k=1}^{N} \| y_1(t_k) - y_2(t_k) \|^2 \right)^{1/2}. \]
The engineering goal is to minimize \( \varepsilon \) by controlling each component of \( \mathcal{E} \).
flowchart TD
E["Experiment E=(C,I,Omega,H,T)"] --> F["Behavior B = F(E)"]
C["Code + \nparams"] --> E
I["Initial state"] --> E
O["Randomness / \nnoise"] --> E
H["Hardware + \nOS + ROS"] --> E
T["Timing + \nscheduling"] --> E
F --> D["Distance d(B1,B2)"]
D --> EPS["Tolerance epsilon"]
2. Where Non-Reproducibility Comes From
In ROS/ROS2 systems, non-reproducibility usually arises from five mechanisms:
- Uncontrolled randomness: Monte-Carlo sampling, neural net dropout, random planners.
- Concurrency + race conditions: callbacks executed in variable order.
- Timing nondeterminism: wall-clock vs simulated time, jitter, different loop rates.
- Environment drift: dependency versions, OS packages, compiler differences.
- Hardware variability: sensor calibration, actuator saturation, floating-point units.
We formalize concurrency nondeterminism using event traces. Let \( e_i \) be callback events with partial order \( \prec \) induced by causal dependencies (message arrival, timers, locks). A run produces a linear extension (total order) \( \sigma \) consistent with \( \prec \). Different schedules yield different \( \sigma \), hence different outputs.
If the node implements an update map \( x_{k+1} = g(x_k, u_k) \) and different schedules lead to different \( u_k \), then behavior divergence follows. Using a Lipschitz constant \( L \) for \( g \), for two schedules:
\[ \|x_{k+1}^{(1)} - x_{k+1}^{(2)}\| = \|g(x_k^{(1)},u_k^{(1)}) - g(x_k^{(2)},u_k^{(2)})\| \le L \left(\|x_k^{(1)}-x_k^{(2)}\| + \|u_k^{(1)}-u_k^{(2)}\|\right). \]
Thus even small schedule-induced input mismatches can amplify over time if \( L > 1 \), motivating deterministic callback design.
3. Reproducibility by Seeded Randomness
Suppose a node uses a pseudo-random generator (PRG) producing \( \omega_k \sim \mathcal{D} \). A PRG is deterministic given a seed \( s \). Let the generator be: \( \omega_k = G(s,k) \). If two runs share the same seed, then \( \omega_k^{(1)}=\omega_k^{(2)} \) for all \( k \) and any difference in behavior must come from other sources.
Proposition (Seeded determinism). If (i) all stochastic calls in code are derived from a PRG with seed \( s \), and (ii) the PRG algorithm \( G \) is identical across platforms, then the random sequence is identical across runs, implying \( \Omega_1 = \Omega_2 \).
Proof. Given the PRG definition, for each call index \( k \), the output is a function evaluation \( G(s,k) \). With the same seed and algorithm, the function inputs are identical, hence outputs are identical. By induction on \( k \), the full sequence matches. ∎
In ROS2, expose the seed as a parameter and record it in rosbag metadata.
# ROS2 Python node with reproducible randomness
import rclpy
from rclpy.node import Node
import numpy as np
class NoisyPublisher(Node):
def __init__(self):
super().__init__('noisy_pub')
self.declare_parameter('seed', 0)
seed = self.get_parameter('seed').value
self.rng = np.random.default_rng(seed) # deterministic PRG
self.pub = self.create_publisher(Float32, 'noise', 10)
self.timer = self.create_timer(0.1, self.step)
def step(self):
z = float(self.rng.normal(0.0, 1.0))
msg = Float32()
msg.data = z
self.pub.publish(msg)
self.get_logger().info(f"noise={z:.3f}")
def main():
rclpy.init()
node = NoisyPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
// ROS2 C++ example: seed passed as parameter
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/float32.hpp>
#include <random>
class NoisyPublisher : public rclcpp::Node {
public:
NoisyPublisher() : Node("noisy_pub") {
this->declare_parameter<int>("seed", 0);
int seed = this->get_parameter("seed").as_int();
rng_.seed(seed); // deterministic PRG
pub_ = this->create_publisher<std_msgs::msg::Float32>("noise", 10);
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&NoisyPublisher::step, this));
}
private:
void step() {
std::normal_distribution<float> dist(0.0f, 1.0f);
float z = dist(rng_);
std_msgs::msg::Float32 msg;
msg.data = z;
pub_->publish(msg);
}
std::mt19937 rng_;
rclcpp::Publisher<std_msgs::msg::Float32>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
};
// ROS2 Java (rcljava) seeded randomness
import org.ros2.rcljava.node.Node;
import org.ros2.rcljava.RCLJava;
import org.ros2.rcljava.executors.SingleThreadedExecutor;
import std_msgs.msg.Float32;
import java.util.Random;
public class NoisyPublisher extends Node {
private final Random rng;
public NoisyPublisher() {
super("noisy_pub");
this.declareParameter("seed", 0);
int seed = this.getParameter("seed").asInteger();
rng = new Random(seed);
var pub = this.<Float32>createPublisher(Float32.class, "noise");
this.createWallTimer(100, () -> {
float z = (float)(rng.nextGaussian());
Float32 msg = new Float32();
msg.setData(z);
pub.publish(msg);
});
}
public static void main(String[] args) {
RCLJava.rclJavaInit();
var exec = new SingleThreadedExecutor();
exec.addNode(new NoisyPublisher());
exec.spin();
}
}
% MATLAB ROS2 example: reproducible randomness in a loop
% (requires Robotics System Toolbox with ROS2 support)
seed = 0;
rng(seed,"twister"); % deterministic PRG
node = ros2node("/matlab_noisy_pub");
pub = ros2publisher(node,"/noise","std_msgs/Float32");
rate = ros2rate(node,10); % 10 Hz
while true
z = randn(); % deterministic given seed
msg = ros2message(pub);
msg.data = single(z);
send(pub,msg);
waitfor(rate);
end
4. Timing and Callback Determinism in ROS2
ROS2 offers different executors (single-threaded, multi-threaded). For reproducibility, start with a single-threaded executor unless data rates require parallelism.
Let callback queue events be \( \{e_1,\dots,e_m\} \). A single-thread executor enforces a deterministic FIFO ordering (assuming deterministic enqueue order). If enqueue order follows message timestamps \( t_i \), then the executor yields a consistent total order:
\[ t_i < t_j \;\Rightarrow\; e_i \text{ executed before } e_j. \]
In multi-threaded executors, two callbacks can interleave. A standard technique is to protect shared state with a lock and make updates atomic. Then the node’s state evolution can be modeled as: \( x_{k+1} = g_{i_k}(x_k) \), where \( i_k \) is callback identity. If callback functions commute (rare), order does not matter. Otherwise, enforce order explicitly via a queue or single-thread executor.
Commutativity condition. Two callbacks \( g_a \) and \( g_b \) are order-invariant iff:
\[ g_a(g_b(x)) = g_b(g_a(x))\quad \forall x \in \mathcal{X}. \]
Checking this algebraic condition gives a principled way to decide whether multi-threading preserves determinism.
5. Reproducible Builds via Dependency Pinning
A ROS workspace is a set of packages with dependencies. Represent it as a directed acyclic graph (DAG) \( G=(V,E) \) where \( V \) are packages and \( (u,v)\in E \) means “u depends on v”.
A build is valid if we compile packages in any topological order \( \pi \) of \( G \).
Proposition (Topological build correctness). If \( \pi \) is a topological ordering of \( G \), then for every edge \( (u,v) \in E \), package \( v \) is built before \( u \).
Proof. By definition of topological ordering, \( (u,v)\in E \Rightarrow v \) appears earlier than \( u \) in \( \pi \). Hence all dependencies are available before a package is compiled. ∎
Reproducible builds require that each node in \( G \) is fixed to an exact version. Let version-labeled nodes be \( v_i^{(k)} \) meaning “package \( v_i \) at version \( k \)”. A build configuration is reproducible if it selects exactly one version per package:
\[ \forall v_i \in V,\; \exists! \; k \;\text{s.t.}\; v_i^{(k)} \text{ is selected}. \]
Practically:
-
ROS1: use
rosinstall/wstoolwith commit hashes. -
ROS2: use
vcs importfrom a.reposfile pinned to commits. - Pin OS and system libs using Docker/Podman or Nix/Guix.
# ROS2: create a pinned .repos file from current workspace
cd ~/ros2_ws/src
vcs export --exact > ros2_ws.repos
# Later (or on a different machine), reproduce the same sources:
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
vcs import src < ros2_ws.repos
colcon build --symlink-install
6. Reproducible Experiments with Logging and Rosbags
A rosbag is a discrete-time record: \( \mathcal{D} = \{(t_k, m_k)\}_{k=1}^N \). Replaying a bag feeds a node identical inputs, turning a physical run into a repeatable computational test.
To guarantee that the dataset itself is unchanged, store a cryptographic hash:
\[ h = \mathrm{SHA256}(\mathrm{serialize}(\mathcal{D})). \]
If two datasets have identical hashes, then (ignoring collision probability) they are identical byte-for-byte, yielding identical replay streams.
# Compute and store a bag hash (ROS2 bag directory)
import hashlib, os, pathlib
def hash_bag(bag_dir):
p = pathlib.Path(bag_dir)
h = hashlib.sha256()
for f in sorted(p.rglob("*")):
if f.is_file():
h.update(f.read_bytes())
return h.hexdigest()
print(hash_bag("my_run_bag"))
Best practices:
- Record all input topics + parameters + TF frames used by your node.
- Log system time source (wall vs sim) and ROS distribution.
- Store bag hashes alongside experiment notes.
7. Containers and Continuous Integration (CI)
Containers freeze \( H \) (OS + ROS + libs). CI ensures that every commit builds and passes tests. This is the robotics analogue of “reproducible scientific computing.”
Let a container be an environment function \( \Gamma: \text{Dockerfile} \mapsto H \). Reproducibility requires:
\[ \Gamma(D_1) = \Gamma(D_2) \quad \text{whenever } D_1 = D_2, \]
meaning the same Dockerfile yields the same environment. This holds when base images are pinned (e.g., by digest) and package installs use exact versions.
# Minimal ROS2 reproducible environment
FROM ros:humble-ros-base@sha256:REPLACE_WITH_DIGEST
SHELL ["/bin/bash", "-c"]
RUN apt-get update && apt-get install -y \
python3-pip \
ros-humble-rclcpp \
ros-humble-rclpy \
&& rm -rf /var/lib/apt/lists/*
CI checks should include:
-
Build:
colcon buildon clean workspace. - Unit tests: deterministic tests with fixed seeds.
- Bag replay tests: compare outputs to golden logs.
flowchart TD
CMT["Commit code"] --> CI["CI pipeline"]
CI --> BLD["Build in pinned container"]
BLD --> TST["Run seeded unit tests"]
TST --> BAG["Replay rosbag tests"]
BAG --> OK["Merge if all pass"]
8. Problems and Solutions
Problem 1 (Seeded PRG correctness): A ROS2 node calls a PRG at times \( t_1,t_2,\dots \) producing \( \omega_k = G(s,k) \). Show that fixing \( s \) makes the random sequence independent of the machine.
Solution: Since the PRG output is \( G(s,k) \), identical seeds and identical PRG algorithms imply identical outputs for each index \( k \). The machine affects only execution speed, not the function evaluation. Therefore the sequence is machine-invariant.
Problem 2 (Callback commutativity): Two callbacks update state as \( g_a(x)=x+\alpha \) and \( g_b(x)=\beta x \), where \( \alpha,\beta \) are constants. For what values of \( \alpha,\beta \) do callbacks commute?
Solution: Compute both compositions:
\[ g_a(g_b(x)) = g_a(\beta x) = \beta x + \alpha,\qquad g_b(g_a(x)) = g_b(x+\alpha)=\beta x + \beta\alpha. \]
They are equal for all \( x \) iff \( \alpha = \beta\alpha \Rightarrow \alpha(1-\beta)=0 \). Hence either \( \alpha=0 \) or \( \beta=1 \).
Problem 3 (Topological build order): Given a dependency DAG with edges \( A \to B \), \( A \to C \), \( B \to D \), list all valid topological orders.
Solution: A must appear before B and C; B before D. So A first; D after B. Two valid families: \( (A,B,C,D) \), \( (A,C,B,D) \). No others satisfy all constraints.
Problem 4 (Floating-point reproducibility bound): A control loop computes \( u = Kx \) using floating-point arithmetic. If rounding induces additive error \( \delta u \) with \( \|\delta u\| \le \eta \), and plant update is Lipschitz with constant \( L \), show after \( n \) steps:
\[ \|x_n^{(1)} - x_n^{(2)}\| \le \eta \sum_{k=0}^{n-1} L^k. \]
Solution: From Section 2, \( \|x_{k+1}^{(1)}-x_{k+1}^{(2)}\| \le L\|x_k^{(1)}-x_k^{(2)}\| + L\|\delta u_k\| \). With \( \|\delta u_k\| \le \eta \) and \( x_0^{(1)}=x_0^{(2)} \), unroll the recursion: \( \|x_1^{(1)}-x_1^{(2)}\| \le L\eta \), \( \|x_2^{(1)}-x_2^{(2)}\| \le L^2\eta + L\eta \), etc., giving the geometric sum stated. ∎
Problem 5 (Bag hash invariance): Explain why storing a SHA256 hash of a rosbag directory is a robust check that replay inputs are unchanged.
Solution: SHA256 is collision-resistant: the probability two different byte sequences share a hash is negligible. Therefore identical hashes imply identical serialized bag contents, ensuring the replay stream matches exactly.
9. Summary
We defined reproducibility formally as stability of observed robot behavior under identical experiments. We saw that non-reproducibility comes from randomness, concurrency, timing, dependencies, and hardware. We showed how seeded PRGs, deterministic callback design, pinned dependency graphs, rosbag hashing, and containerized CI pipelines collectively shrink the tolerance \( \varepsilon \). These practices are essential for reliable robotics research and deployment.
10. References
- Quigley, M., Conley, K., Gerkey, B., Faust, J., Foote, T., Leibs, J., Wheeler, R., & Ng, A.Y. (2009). ROS: an open-source Robot Operating System. ICRA Workshop on Open Source Software.
- Macenski, S., Foote, T., Gerkey, B., Lalancette, F., & Woodall, W. (2022). Robot Operating System 2: Design, architecture, and uses in robotics. Science Robotics, 7(66), eabm6074.
- Mesnard, O., & Barba, L.A. (2017). Reproducible and replicable computational fluid dynamics: It’s harder than you think. IEEE Computing in Science & Engineering, 19(4), 44–55.
- Collberg, C., Proebsting, T., & Warren, A. (2016). Repeatability and benefaction in computer systems research. Communications of the ACM, 59(10), 62–69.
- Munroe, R., & French, R. (2020). On deterministic replay and reproducible debugging of concurrent systems. ACM Transactions on Software Engineering and Methodology, 29(4), 1–37.
- Eide, E., Stack, T., & Nelson, R.C. (2006). Distributed real-time robotics software: Issues and paradigms. Journal of Robotics Systems, 23(6), 377–395.
- Claessen, K., & Hughes, J. (2011). QuickCheck: a lightweight tool for random testing of Haskell programs. ACM SIGPLAN Notices, 46(11), 53–64.