Tools for optimizing and executing the CONCERT welding trajectory.
The workflow is split into two parts:
- Offline: run
weld_opt.py, optionally replay withreplayer.py, and inspect the RViz visualization. - Online: launch simulation, start XBot GUI, publish the current gap pose, compensate gravity, home to the optimized start, drive the base to the optimized pose, and run the controller.
Start the Docker container before running the offline or online pipeline:
xhost +local:docker
cd /home/user/concert_ws/src/acea_concert/docker
docker compose up -d --build
docker compose exec dev bashOpen another terminal in the same running container when needed:
cd /home/user/concert_ws/src/acea_concert/docker
docker compose exec dev bashStop the container when finished:
cd /home/user/concert_ws/src/acea_concert/docker
docker compose downCommand pipeline:
cd /home/user/concert_ws/src/acea_concert
python3 src/weld_opt.py
python3 src/plan_homing_from_mat.py
python3 src/replayer.py
rviz2 -d rviz/rviz_config.rvizRun the optimizer from the package root:
cd /home/user/concert_ws/src/acea_concert
python3 src/weld_opt.pysrc/weld_opt.py solves the Horizon optimization problem and saves:
mat_files/weld_concert.mat
The MAT file contains the optimized joint trajectory, the pipe/gap geometry, and the weld trajectory expressed in the gap frame. The online controller expects this file to exist before starting the simulation.
Optionally plan a collision-aware homing trajectory and save it back into the MAT file as q_homing:
cd /home/user/concert_ws/src/acea_concert
python3 src/plan_homing_from_mat.pyOptionally replay the optimized trajectory:
cd /home/user/concert_ws/src/acea_concert
python3 src/replayer.pyUse RViz to inspect the robot, pipe, and optimized weld trajectory:
rviz2 -d rviz/rviz_config.rvizStart the standalone container with its default isolated Docker network:
docker compose -f docker/compose.yaml up -dTo communicate with ROS 2 outside the container, set the required host IP and use the ROS launcher:
ROS2_IP=<host-IP-on-the-robot-network> ROS_DOMAIN_ID=0 docker/up-rosThe external ROS 2 machine must use the same ROS_DOMAIN_ID and
RMW_IMPLEMENTATION=rmw_cyclonedds_cpp.
Shut down either mode without setting ROS2_IP:
# Standalone
docker compose -f docker/compose.yaml down
# External ROS networking
docker compose \
-f docker/compose.yaml \
-f docker/compose.ros.yaml \
downStart each component in a separate terminal.
Run in this order:
weld_sim.launch.py- XBot GUI
- gap pose GUI / publisher
gravity_comp_node.pyhome_to_weld_start.pydrive_base_to_weld_pose.pycontroller.py
Open a terminal and connect to the container:
docker exec -it [container-id] bash
If you have tmux, create one ready-to-launch terminal session:
cd /home/user/concert_ws/src/acea_concert
scripts/concert_tmuxThe script opens one tiled tmux window with named panes, sources the
ROS/XBot/workspace environment in each pane, and leaves the right command typed.
Click panes and press Enter in the order listed above. If you prefer tmux tabs,
run scripts/concert_tmux --windows.
For a normal terminal, source the same environment manually:
cd /home/user/concert_ws/src/acea_concert
source scripts/concert_env.bashcd /home/user/concert_ws
ros2 launch acea_concert weld_sim.launch.pyThis launches the robot simulation and spawns the two pipe halves using the geometry stored in mat_files/weld_concert.mat.
To use a different optimization result, pass mat_file. To start with the gap at the optimized pose relative to the robot, add optimized_robot_pose:=true:
ros2 launch acea_concert weld_sim.launch.py mat_file:=mat_files/weld_concert.mat optimized_robot_pose:=trueStart the XBot GUI after the simulation is running.
cd /home/user/concert_ws/src/acea_concert
python3 src/gap_pose_publisher.pyOptional test faults can be added to the published /gap/pose_robot:
python3 src/gap_pose_publisher.py \
--gap-pos-noise-std 0.01 \
--gap-rot-noise-std 0.02 \
--gap-disconnect-every 10 \
--gap-disconnect-duration 2This publishes the current gap pose with respect to base_link:
/gap/pose_robot
This node is ground truth for simulation. In the real setup, it should be replaced by perception.
cd /home/user/concert_ws/src/acea_concert
python3 src/gravity_comp_node.pycd /home/user/concert_ws/src/acea_concert
python3 src/home_to_weld_start.pycd /home/user/concert_ws/src/acea_concert
python3 src/drive_base_to_weld_pose.pycd /home/user/concert_ws/src/acea_concert
python3 src/controller.pyTo smooth noisy camera gap poses, enable the controller-side low-pass filter:
python3 src/controller.py --gap-filter-tau 0.1With scripts/concert_env.bash sourced, the same command is:
concert_controller --gap-filter-tau 0.10 disables the filter. A small value like 0.05-0.15 seconds is a good starting range.
To replay the optimized trajectory without /gap/pose_robot, run open-loop:
concert_controller --open-loopFor slow welding with noisy camera poses, use a history window and reject big camera jumps:
concert_controller \
--gap-filter-history-size 50 \
--gap-filter-tau 0.3 \
--gap-filter-max-position-jump 0.02 \
--gap-filter-max-angle-jump 10The controller uses:
optimized posture trajectory from the MAT file
desired weld position in the gap frame
desired weld orientation in the gap frame
measured /gap/pose_robot from ground truth/perception
/gap/pose_robot is the only online gap input required by the controller. It is a PoseStamped in base_link: the position gives base_p_gap, and the quaternion gives base_R_gap.
At runtime it relocates the optimized weld trajectory onto the current measured gap:
position: base_p_ee_des = base_p_gap + base_R_gap * gap_p_ee_des
orientation: base_R_ee_des = base_R_gap * gap_R_ee_des
The controller does not blindly replay the optimized joint trajectory. The joint trajectory is used as a nominal posture, while the welding target is rebuilt online from the measured gap pose.
flowchart TD
subgraph offline["offline optimization"]
mat["weld_concert.mat"]
qdes["q_des(t)<br/>optimal posture"]
weldgap["gap_p_ee_des(t)<br/>gap_R_ee_des(t)<br/>weld target in gap frame"]
mat --> qdes
mat --> weldgap
end
subgraph online["online measurements"]
perception["/gap/pose_robot<br/>ground truth now<br/>perception later"]
gappose["base_p_gap<br/>base_R_gap<br/>derived from pose"]
feedback["robot feedback<br/>q_meas<br/>base_p_ee_meas<br/>base_R_ee_meas"]
perception --> gappose
end
subgraph controller["controller.py"]
relocate["relocate weld target<br/>base_p_ee_des = base_p_gap + base_R_gap * gap_p_ee_des<br/>base_R_ee_des = base_R_gap * gap_R_ee_des"]
project["project error in gap frame<br/>normal direction<br/>tangent direction"]
pd["PD correction<br/>normal velocity<br/>tangent velocity"]
ik["CartesIO IK<br/>EE pose task<br/>postural task"]
end
output["joint position command"]
diag["diagnostics<br/>PlotJuggler"]
qdes --> ik
weldgap --> relocate
gappose --> relocate
gappose --> project
feedback --> project
relocate --> project
project --> pd
pd --> ik
relocate --> ik
ik --> output
output --> feedback
project --> diag
pd --> diag
From the MAT file:
q_des(t) optimized joint posture trajectory
gap_p_ee_des(t) desired weld position expressed in the gap frame
gap_R_ee_des(t) desired tool orientation expressed in the gap frame
From ground truth now, and from perception later:
/gap/pose_robot PoseStamped in base_link
base_p_gap gap origin from /gap/pose_robot.position
base_R_gap gap orientation from /gap/pose_robot.orientation
From robot feedback:
q_meas measured robot joint state
base_p_ee_meas measured EE position from forward kinematics
base_R_ee_meas measured EE orientation from forward kinematics
At each controller tick:
- Read
q_des(t)and send it to the CartesIO postural task. - Read the desired weld position and orientation in the gap frame.
- Read the current measured
/gap/pose_robotinbase_link. - Derive
base_p_gapandbase_R_gap, then convert the desired weld target from gap frame tobase_link. - Compare the measured EE position with the desired weld target along:
- the gap normal direction, across the pipes
- the gap tangent direction, along the weld
- Compute PD correction velocities along those two directions.
- Command the EE pose task and postural task through CartesIO.
- Publish diagnostics for PlotJuggler.
The key idea is that if the robot or gap moves, the weld target moves with the measured gap:
base_p_ee_des = base_p_gap + base_R_gap * gap_p_ee_des
base_R_ee_des = base_R_gap * gap_R_ee_des
src/weld_opt.py: offline trajectory optimization and MAT file generation.src/plan_homing_from_mat.py: adds a collision-awareq_homingtrajectory to a MAT file.src/replayer.py: replay and RViz visualization of the optimized trajectory.launch/weld_sim.launch.py: simulation launch file; acceptsmat_fileandoptimized_start.src/gap_pose_publisher.py: simulation ground-truth gap pose publisher.src/gravity_comp_node.py: gravity compensation node.src/home_to_weld_start.py: moves weld joints to trajectory node 0.src/drive_base_to_weld_pose.py: drives the mobile base to the optimized relative gap pose.src/controller.py: online welding controller.src/controller_ros.py: ROS interface for the controller.src/utils/: controller geometry, trajectory, and diagnostic helpers.
config/weld.yaml: Horizon optimization task configuration.config/cartesio_stack.yaml: CartesIO stack used by the online controller.src/modular/: robot model generation.mat_files/: generated optimization results.
- ROS 2 Jazzy
- Horizon
- CasADi
- XBot2
- CartesIO
- NumPy, SciPy, Matplotlib