Skip to content

Commit e7d548a

Browse files
kaiaaiclaude
andcommitted
oomwoo_clean/nav: add localization:=truth (perfect sim debug pose)
The sim's odometry is noise-free and world-referenced, so localization error is not odometry drift -- it is AMCL wandering as it fits scans to a map that may not match the robot's LiDAR. localization:=truth (sim only) skips AMCL and publishes a static identity map->odom; map->base then tracks the true pose exactly, forever, isolating navigation/map/tuning from localization. localization:=amcl (default) is unchanged. Verified headless: truth -> map->odom identity, map->base = true spawn (-2,-0.5), goal SUCCEEDED; amcl -> localizes ~2s, goal SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0067100 commit e7d548a

2 files changed

Lines changed: 96 additions & 41 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ unknown — seed it with RViz's **2D Pose Estimate**) and leave `coverage` off:
112112
coverage marking is ground-truth based, so it works in sim only (a real robot
113113
needs a belief-based estimator, not built yet).
114114

115+
**Debugging with perfect localization.** The sim's odometry is noise-free, so
116+
you can take AMCL (and any map/scan mismatch) out of the picture entirely:
117+
118+
```bash
119+
ros2 launch oomwoo_clean nav.launch.py use_sim_time:=true localization:=truth
120+
```
121+
122+
`localization:=truth` (sim only) skips AMCL and publishes a static identity
123+
`map→odom`, so `map→base` tracks the true pose exactly — useful for isolating
124+
navigation, map, or tuning issues from localization error.
125+
115126
## Quickstart — reproduce the regressions
116127

117128
Copy-paste. Needs Docker on a native **x86-64 Linux** box (not ARM / not an

src/oomwoo_clean/launch/nav.launch.py

Lines changed: 85 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,27 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
"""
15-
Nav2 + AMCL on an existing map, for the cleaning-with-map workflow.
15+
Nav2 on an existing map, for the cleaning-with-map workflow.
1616
1717
Localization and navigation ONLY -- no Gazebo, no RViz -- so it composes with a
1818
separately-launched robot source (`oomwoo_gazebo world.launch.py` in sim, or
1919
`oomwoo_bringup physical.launch.py` on a real robot) and a separate viewer
2020
(`oomwoo_clean_ui cleaning_debug.launch.py`). Uses the selected robot's own
2121
navigation.yaml, so the same command follows `kaia use <robot>`.
2222
23-
auto_localize:=true (default) seeds AMCL at the spawn pose so the sim localizes
24-
itself -- no manual RViz 2D Pose Estimate. Set it false on a real robot (you do
25-
not know the pose there) and seed with the 2D Pose Estimate instead.
26-
27-
coverage:=true adds the ground-truth coverage meter that marks the floor clean
28-
as the robot drives. That meter is ground-truth based, so it is SIM ONLY; on a
29-
real robot leave coverage off (a belief-based estimator does not exist yet). Both
30-
auto_localize and coverage use the spawn pose, which must match the sim's
31-
(world.launch.py defaults), so the robot localizes and the covered cells line up
32-
with the map.
23+
localization:=
24+
amcl (default) -- Nav2 AMCL scan-matches against the map. Real, but only as
25+
good as the map: if the loaded map does not match the robot's LiDAR,
26+
AMCL's estimate wanders. auto_localize:=true seeds it at the spawn pose
27+
(no manual RViz 2D Pose Estimate); set false on a real robot.
28+
truth -- DEBUG, SIM ONLY. No AMCL: publish a static map->odom at the spawn
29+
pose. The sim's odometry is noise-free, so map->base then tracks the
30+
true pose exactly, forever. Use it to take localization error out of the
31+
picture and debug navigation / the map / tuning on their own.
32+
33+
coverage:=true adds the ground-truth coverage meter (sim only). auto_localize,
34+
coverage and the truth transform all use the spawn pose, which must match the
35+
sim's (world.launch.py defaults) so everything lines up with the map.
3336
"""
3437

3538
import os
@@ -47,7 +50,41 @@
4750
from launch_ros.parameter_descriptions import ParameterValue
4851

4952

50-
def _nav(context, robot_model, map_yaml, use_sim_time):
53+
def _seed_node(xv, yv, yawv):
54+
# (Re)publish /initialpose until AMCL localizes, then exit. The robot's AMCL
55+
# does not self-seed, so this is what puts map->odom on the tree in sim.
56+
return Node(
57+
package='oomwoo_sim_support', executable='initialpose_pub',
58+
name='initialpose_pub', output='screen',
59+
parameters=[{'use_sim_time': True, 'reseed_after_sec': 1.0,
60+
'x': xv, 'y': yv, 'yaw': yawv}])
61+
62+
63+
def _truth_localization(mapf, use_sim):
64+
# Perfect debug localization: a static IDENTITY map->odom. The sim's
65+
# odometry is noise-free AND world-referenced (odom->base already reports
66+
# the true world pose), and the map is world-aligned, so map == odom == world
67+
# -- map->base then equals the true pose for all time, no AMCL, no
68+
# scan-vs-map fitting.
69+
map_server = Node(
70+
package='nav2_map_server', executable='map_server', name='map_server',
71+
output='screen',
72+
parameters=[{'yaml_filename': mapf, 'use_sim_time': use_sim,
73+
'topic_name': 'map', 'frame_id': 'map'}])
74+
lifecycle = Node(
75+
package='nav2_lifecycle_manager', executable='lifecycle_manager',
76+
name='lifecycle_manager_localization', output='screen',
77+
parameters=[{'use_sim_time': use_sim, 'autostart': True,
78+
'node_names': ['map_server']}])
79+
map_odom = Node(
80+
package='tf2_ros', executable='static_transform_publisher',
81+
name='map_odom_truth', output='screen',
82+
arguments=['--frame-id', 'map', '--child-frame-id', 'odom'])
83+
return [map_server, lifecycle, map_odom]
84+
85+
86+
def _nav(context, robot_model, map_yaml, use_sim_time, localization,
87+
auto_localize, x0, y0, yaw0):
5188
model = context.perform_substitution(robot_model)
5289
if not model:
5390
try:
@@ -57,16 +94,32 @@ def _nav(context, robot_model, map_yaml, use_sim_time):
5794
model = 'oomwoo_one'
5895
params = os.path.join(
5996
get_package_share_directory(model), 'config', 'navigation.yaml')
60-
bringup = os.path.join(
61-
get_package_share_directory('nav2_bringup'),
62-
'launch', 'bringup_launch.py')
63-
return [IncludeLaunchDescription(
64-
PythonLaunchDescriptionSource(bringup),
65-
launch_arguments={
66-
'map': context.perform_substitution(map_yaml),
67-
'use_sim_time': context.perform_substitution(use_sim_time),
68-
'params_file': params,
69-
'slam': 'False'}.items())]
97+
launch_dir = os.path.join(
98+
get_package_share_directory('nav2_bringup'), 'launch')
99+
mapf = context.perform_substitution(map_yaml)
100+
sim = context.perform_substitution(use_sim_time)
101+
xv = float(context.perform_substitution(x0))
102+
yv = float(context.perform_substitution(y0))
103+
yawv = float(context.perform_substitution(yaw0))
104+
105+
if context.perform_substitution(localization) == 'truth':
106+
nav = IncludeLaunchDescription(
107+
PythonLaunchDescriptionSource(
108+
os.path.join(launch_dir, 'navigation_launch.py')),
109+
launch_arguments={'use_sim_time': sim,
110+
'params_file': params}.items())
111+
return _truth_localization(mapf, sim.lower() == 'true') + [nav]
112+
113+
# amcl (default): the full Nav2 bringup (map_server + AMCL + navigation)
114+
bringup = IncludeLaunchDescription(
115+
PythonLaunchDescriptionSource(
116+
os.path.join(launch_dir, 'bringup_launch.py')),
117+
launch_arguments={'map': mapf, 'use_sim_time': sim,
118+
'params_file': params, 'slam': 'False'}.items())
119+
actions = [bringup]
120+
if context.perform_substitution(auto_localize).lower() in ('true', '1'):
121+
actions.append(_seed_node(xv, yv, yawv))
122+
return actions
70123

71124

72125
def generate_launch_description() -> LaunchDescription:
@@ -90,12 +143,14 @@ def generate_launch_description() -> LaunchDescription:
90143
DeclareLaunchArgument('robot_model', default_value=''),
91144
DeclareLaunchArgument('map', default_value=default_map),
92145
DeclareLaunchArgument('use_sim_time', default_value='true'),
93-
# auto-seed AMCL at the spawn pose (sim); false on a real robot
146+
# 'amcl' (real) or 'truth' (perfect static map->odom, sim debug)
147+
DeclareLaunchArgument('localization', default_value='amcl',
148+
choices=['amcl', 'truth']),
149+
# auto-seed AMCL at the spawn pose (amcl mode, sim); false on a robot
94150
DeclareLaunchArgument('auto_localize', default_value='true'),
95151
# ground-truth coverage marking: sim only, off by default
96152
DeclareLaunchArgument('coverage', default_value='false'),
97-
# must match the sim spawn (world.launch.py defaults) so the robot
98-
# localizes and coverage aligns
153+
# must match the sim spawn (world.launch.py defaults)
99154
DeclareLaunchArgument('x_pose', default_value='-2.0'),
100155
DeclareLaunchArgument('y_pose', default_value='-0.5'),
101156
DeclareLaunchArgument('yaw', default_value='0.0'),
@@ -104,7 +159,10 @@ def generate_launch_description() -> LaunchDescription:
104159
nav = OpaqueFunction(function=_nav, args=[
105160
LaunchConfiguration('robot_model'),
106161
LaunchConfiguration('map'),
107-
LaunchConfiguration('use_sim_time')])
162+
LaunchConfiguration('use_sim_time'),
163+
LaunchConfiguration('localization'),
164+
LaunchConfiguration('auto_localize'),
165+
x0, y0, yaw0])
108166

109167
with_coverage = IfCondition(LaunchConfiguration('coverage'))
110168
ground_truth = Node(
@@ -125,18 +183,4 @@ def generate_launch_description() -> LaunchDescription:
125183
('ground_truth/pose', '/ground_truth/pose'),
126184
('cleaning_active', '/coverage_planner/cleaning_active')])
127185

128-
# Seed AMCL at the spawn pose so the sim localizes without a manual RViz
129-
# 2D Pose Estimate. initialpose_pub (re)publishes /initialpose until AMCL
130-
# localizes, then exits; the robot's AMCL does not self-seed, so this is
131-
# what puts map->odom on the tree.
132-
seed = Node(
133-
package='oomwoo_sim_support', executable='initialpose_pub',
134-
name='initialpose_pub', output='screen',
135-
condition=IfCondition(LaunchConfiguration('auto_localize')),
136-
parameters=[{'use_sim_time': True, 'reseed_after_sec': 1.0,
137-
'x': ParameterValue(x0, value_type=float),
138-
'y': ParameterValue(y0, value_type=float),
139-
'yaw': ParameterValue(yaw0, value_type=float)}])
140-
141-
return LaunchDescription(
142-
args + [nav, ground_truth, coverage_meter, seed])
186+
return LaunchDescription(args + [nav, ground_truth, coverage_meter])

0 commit comments

Comments
 (0)