Aaryan Agrawal

Mapping & Localization

From “odometry drifts, fixes correct it” to whiteboard-cold: Bayes filters, odometry sources, map representations, ICP/AMCL/fiducials, graph SLAM, and the robustness engineering production robots actually run.

July 2026 · robotics · SLAM · state estimation

1. Poses, frames, and the TF tree

A pose is an element of a Lie group: SE(2) = (x, y, θ) for planar robots, SE(3) = 3D position + 3D orientation for everything else. The "group" part matters practically: poses compose by multiplication, not addition. T_world_camera = T_world_base · T_base_camera. Get the order wrong and everything is subtly garbage.

Rotations are the tricky half. Euler angles (roll/pitch/yaw) are readable but suffer gimbal lock — at pitch = 90° two axes align and you lose a degree of freedom, so interpolation and composition break. Production code stores quaternions (4 numbers, unit norm, no singularities, cheap to compose) and converts to Euler only for printing. Rule: never average quaternions naively, never add angles across ±π without wrapping. Errors and small corrections live in the tangent space (rotation vectors / twists) — that's what "SE(3) log/exp" means when you see it in optimizer docs.

The canonical frame chain:

world/map ──(localization: slow, absolute, can jump)── odom ──(odometry: fast, smooth, drifts)── base_link ──(static calib)── sensor frames
  • odom → base_link: published by dead reckoning at high rate. Continuous — never jumps. Everything reactive (control loops, obstacle avoidance) consumes this because a discontinuity here would jerk the robot.
  • map → odom: published by the localizer. This is the accumulated-drift estimate. It can jump — and that's fine, because planners consume map → base_link (the composition) while controllers consume odom → base_link.

Why correct via map → odom instead of "teleporting" the robot's pose: it decouples rates and failure domains. Odometry keeps streaming at 200 Hz even if the localizer dies; a localization fix arriving 300 ms late still applies cleanly because the correction composes with the odometry that accumulated meanwhile; and a bad fix corrupts one transform you can gate/rate-limit, not the robot's motion estimate.

Extrinsics (where each sensor sits on the body) are static transforms in the same tree, from CAD or calibration. A 1° error in camera mounting yaw puts a landmark seen at 5 m about 9 cm off to the side — extrinsic errors masquerade as localization bugs constantly.

Dimensional: dimos has a real TF tree (dimos/protocol/tf/tf.py, self.tf.publish/get, 10 s temporal buffer). Go2 frames: world → base_link → camera_link → camera_optical; relocalization adds world → map (naming is inverted vs ROS convention — their world is the odom origin; don't let it trip you). Your module publishes exactly that correction transform.

2. Probabilistic foundations

Pose is a belief — a probability distribution over where the robot could be — because both motion and sensing are noisy. All localization algorithms are one loop, the Bayes filter, alternating:

  • Predict (motion model): bel⁻(xₜ) = ∫ p(xₜ | xₜ₋₁, uₜ) · bel(xₜ₋₁) dxₜ₋₁ — push the belief through the commanded/measured motion; uncertainty grows.
  • Update (measurement model): bel(xₜ) = η · p(zₜ | xₜ) · bel⁻(xₜ) — reweight by how well each hypothetical pose explains the observation; uncertainty shrinks.

Every named algorithm is a choice of how to represent bel(x):

Gaussian filters represent it as mean + covariance. The Kalman filter is the exact solution when motion and measurement are linear with Gaussian noise. Robots are nonlinear (rotations!), so the EKF linearizes with Jacobians at the current mean — cheap and fine while the belief stays tight and unimodal, but the linearization lies when uncertainty is large or the function curves hard (bearing measurements at close range, heading uncertainty > ~30°). The UKF propagates a handful of sigma points through the true nonlinear function instead — better accuracy for the same unimodal-Gaussian limitation.

Particle filters represent bel(x) as N weighted samples. Predict = move each particle with sampled motion noise; update = weight each by measurement likelihood; resample to concentrate particles where the probability is. Superpower: multimodal beliefs — "I'm at one of these four identical corridor junctions" is representable, which no Gaussian can do. Costs: N scales badly with dimension (fine in SE(2), painful in SE(3)); particle deprivation — resampling can kill the true hypothesis if it briefly scored poorly, which is why implementations inject random particles or resample lazily (only when effective sample size drops).

Covariance is the currency of fusion. Every source (odom, IMU, tag fix) reports uncertainty; the filter weights inversely by it. A source that lies about its covariance ("I'm perfect ± 1 mm") poisons the fusion. Most real "sensor fusion bugs" are miscalibrated covariances, not code.

Dimensional: dimos messages carry the slots for this (PoseWithCovarianceStamped, Odometry has covariance). Your marker fix should publish honest covariance — grows with tag distance² and viewing angle — so downstream gating is principled instead of vibes.

3. Odometry sources

Rough drift numbers (order-of-magnitude, indoor, decent tuning):

SourceRateDriftFailure flavor
Wheel encoders50–500 Hz1–5 % of distance; yaw is the killerslip, kidnap-blind
MEMS IMU alone200–1000 Hzmeters within ~10 sbias random walk, position ∝ t²
Visual odometry (mono)15–30 Hz1–3 % + unknown scaletexture-less walls, motion blur, lighting
VIO (camera+IMU)30 Hz out0.1–1 % of trajectoryaggressive motion + featureless together
Lidar odometry (FAST-LIO2 class)10–50 Hz~0.1–0.5 %geometry-poor spaces (long corridor, open lot)
Legged kinematic odom (Go2/G1)200+ Hzworse than wheels; several % + yaw driftfoot slip, contact misdetection
  • Wheel: integrate encoder ticks through the drive kinematics. Heading error dominates — 1° of yaw error puts you ~1.7 cm/m sideways, and yaw errors integrate. Carpet, slip, tire wear all bias it.
  • IMU: accelerometers must be double-integrated, and gyro/accel biases wander (random walk), so IMU-only position explodes in seconds. IMUs are never a pose source alone; they're the high-rate glue. Preintegration (Forster et al.) is the trick that made modern VIO: summarize hundreds of IMU samples between camera frames into one relative-motion constraint that can be cheaply re-linearized when the bias estimate changes.
  • Visual odometry: track features frame-to-frame, estimate relative motion. Monocular VO recovers direction of translation but not its magnitude — scale is unobservable (§ scale ambiguity) and also drifts over time.
  • VIO is the magic combo because the failure modes are complementary: the IMU observes metric scale (accelerations are in m/s²) and covers fast motion/blur; the camera observes structure and kills the IMU's integration drift. This is what a phone's ARKit runs.
  • Lidar / lidar-inertial odometry: scan-match consecutive sweeps, IMU for de-warping motion. Best-in-class drift; dies where geometry is ambiguous — a long featureless corridor constrains 5 of 6 DOF and lets you slide along the sixth.
  • Legged kinematic odom: fuse joint encoders + estimated foot contacts + IMU. Feet slip and contact detection is noisy, so it's the drifty-est of the "good" sources — exactly why the Go2 stack needs correction.

Dimensional: the Go2's onboard odom (GO2Connection, dimos/robot/unitree/go2/connection.py → TF world → base_link) is the whole localization story today, and their own docs admit it: "We don't have proper loop closure and stable odometry… it does drift eventually" (docs/capabilities/navigation/deep_dive.md), rated OK to ~500 m². FastLIO2/PointLIO exist as native modules for Livox rigs. Your module is the drift-killer for the camera-only path.

4. Map representations

A "map" is whatever data structure your measurement model can score a pose against.

  • Occupancy grid (2D): each cell holds P(occupied), stored as log-odds so a sensor update is one addition: l ← l + log(p/(1−p)) − l₀, clamped. 5 cm cells typical indoors. The lingua franca of 2D nav (AMCL localizes against it, costmaps derive from it).
  • Voxels / octrees (OctoMap): the 3D generalization; octrees make empty space cheap. dimos's VoxelGridMapper (dimos/mapping/voxels.py) is this family — lidar → global voxel map, CUDA-accelerated.
  • TSDF/ESDF: each voxel stores signed distance to the nearest surface (truncated near it). TSDF = smooth surface reconstruction (KinectFusion lineage); ESDF = distance-to-obstacle everywhere, which trajectory optimizers want as a differentiable cost.
  • Point-cloud maps: just the registered points (+ normals). What ICP-style localization matches against directly. dimos's relocalization premap (.pc2.lcm files) is this.
  • Landmark / feature maps: a list of discrete things with poses — visual features with descriptors, or fiducial tags: your marker map is exactly this, the smallest and most robust member of the family. Kilobytes, human-auditable ("tag 17 is by the elevator"), trivially editable.
  • Topological maps: graph of places + traversability edges, no metric global consistency needed. How large facilities scale ("metric locally, topological globally").
  • Semantic / scene-graph maps: objects, rooms, relations. This is dimos "spatial memory" / spatio-temporal RAG territory — an agent asking "where is the kitchen" queries this layer, which sits on top of metric localization, and is only as good as the pose that anchored each observation.
  • Costmaps are not maps — they're planning artifacts derived from a map + inflation radii + robot footprint (dimos: dimos/mapping/costmapper.py derives OccupancyGrid costs from the voxel map's height gradients).
  • NeRF / 3D Gaussian Splatting: research edge — photorealistic implicit maps you can localize against by render-and-compare. Compelling demos, not production service-robot tech in 2026 (compute, map maintenance, robustness).

Dimensional: you're adding the landmark-map representation to a stack that today has only voxel/point-cloud maps. It composes: tag map anchors global pose; voxel map keeps doing obstacles.

5. Localization in a known map

Scan matching. ICP (iterative closest point): given a live scan and map, alternately (1) associate each scan point with its nearest map point, (2) solve the rigid transform minimizing Σ‖T·pᵢ − qᵢ‖² (closed form via SVD), repeat. Point-to-plane variant minimizes distance along the map normal instead — converges much faster on structured indoor geometry. The catch: nearest-neighbor association is only right if you start close, so ICP is a local refiner — bad initial guess (rough guide: worse than a few tens of cm / ~20–30°) → wrong basin, confidently wrong answer. That's why ICP pipelines gate on fitness (inlier fraction / mean residual) and why they need something else for global initialization. NDT models the map as a grid of local Gaussians and optimizes a smooth likelihood — wider convergence basin, popular in automotive.

Monte Carlo Localization (AMCL) — the particle filter applied to a 2D grid map, end-to-end:

  1. Init: particles spread over the map (global) or around a guess (tracking). Global init needs ~10k+ particles; tracking runs at 500–5000.
  2. Predict: move every particle by odometry + sampled noise.
  3. Weight: for each particle, raycast the map from that pose and score the actual lidar scan against the expectation (likelihood-field model in practice — precomputed distance-to-nearest-obstacle makes this fast and smooth).
  4. Adaptive resampling: KLD-sampling grows/shrinks N with belief spread; only resample when effective sample size drops.
  5. Kidnapped-robot recovery: track short-vs-long-term average likelihood; when the filter is consistently surprised, inject random particles ∝ the mismatch, letting the true pose be rediscovered.

Fiducial localization — your project. A tag is a landmark whose 4 corner positions in its own frame are known exactly (side length s). Detect corners in the image → PnP: find camera pose minimizing reprojection error Σ‖π(K·T·Xᵢ) − xᵢ‖² over the 4 correspondences (OpenCV IPPE_SQUARE exploits the planar-square structure; it returns the two-fold ambiguity for near-frontal views — handle it). Then T_world_camera = T_world_tag · T_tag_camera and chain extrinsics to base. Accuracy: cm-level position and 1–2° within a few meters for a properly sized tag; depth error grows ~distance² while bearing stays good; oblique viewing beyond ~60° and motion blur are the practical killers. Multi-tag in one frame → solve one joint PnP over all corners (much stronger, kills the ambiguity). Tag-map building (tagSLAM idea): drive around observing tags from many poses, build a factor graph of camera-tag observations, optimize jointly → globally consistent tag map even though each drive-by pose drifted.

Global relocalization / place recognition — answering "where am I, with no prior": bag-of-visual-words vocabularies (DBoW, what ORB-SLAM uses), learned global descriptors (NetVLAD), lidar Scan Context. Retrieval gives a coarse candidate; ICP/PnP refines. Note that fiducials make this trivial — one tag sighting IS global relocalization.

Dimensional: dimos has exactly one localizer today — RelocalizationModule (dimos/mapping/relocalization/module.py): ICP of the live voxel map against a premap every 2 s, fitness-gated, publishes TF world → map. It has the local-refiner weakness (needs a decent initial guess, can't recover from kidnap). Your tag module is its complement: absolute, kidnap-proof, works where lidar geometry lies.

6. SLAM proper

The chicken-and-egg, precisely: localization needs a map (measurement model scores poses against something), mapping needs poses (to place observations). SLAM estimates the joint posterior p(trajectory, map | measurements, controls). The joint is what makes it hard — pose error and map error are correlated, so you can't just alternate naively.

EKF-SLAM (1990s): one giant Gaussian over robot pose + every landmark. Died because the covariance matrix is dense — O(n²) memory, O(n²) per update — and one wrong data association (observation matched to the wrong landmark) corrupts the whole state irrecoverably.

FastSLAM: Rao-Blackwellized particle filter — each particle carries a trajectory hypothesis plus its own map (conditioned on a known trajectory, landmarks decorrelate → n tiny independent EKFs). Historically important (gmapping is this, with grid maps); superseded for large scale.

Graph SLAM — the modern standard. Reframe as sparse nonlinear least squares:

  • Nodes = robot poses over time (+ optionally landmarks).
  • Edges = constraints: odometry between consecutive poses, landmark observations, loop closures.
  • Solve: minimize Σᵢ ‖eᵢ(x)‖²_Σᵢ (errors weighted by information matrices) with Gauss-Newton/Levenberg-Marquardt. The graph is sparse, so this scales to millions of variables. Libraries: GTSAM (factor graphs, iSAM2 incremental), g2o, Ceres.
x0 ──odom── x1 ──odom── x2 ──odom── x3 ── … ── x9
 \                        |                    /
  \──obs── tag17 ──obs────┘     loop closure ─/   (x9 recognizes x0's place)

Split into front-end (perception: extract constraints — feature matching, scan matching, place recognition, data association) and back-end (optimization over the graph). Most SLAM failures are front-end failures fed to a trusting back-end.

Loop closure is THE event. Pure odometry chains accumulate unbounded error; the first constraint between temporally distant poses ("this is the same corner I saw 10 minutes ago") lets the optimizer redistribute the accumulated drift around the whole loop — the map snaps from banana-shaped to consistent. Corollary: a wrong loop closure snaps the map into confident garbage. Defenses: geometric verification before accepting (does ICP between the two scans actually converge with high fitness?), robust kernels (Huber/Cauchy — cap any single edge's influence), switchable constraints (optimizer can learn to disbelieve an edge).

Online use: full-graph optimization grows forever, so real-time systems optimize a sliding window and marginalize old states (absorb them into a prior). Marginalization is where "why is my VIO slowly inconsistent" bugs live — but that's beyond what you need for the trial.

Dimensional: dimos runs pose-graph optimization offline only, at map-build time (dimos/mapping/loop_closure/pgo.py, dimos map global <db> --export). Live, there is no loop closure — which is precisely the admitted gap. Your tag observations are loop-closure-grade constraints for free (data association is solved: the tag broadcasts its ID).

7. The named systems people actually cite

Lidar:

  • gmapping — 2D FastSLAM on occupancy grids. The 2010s workhorse, still fine for small single-floor maps; superseded.
  • Cartographer (Google) — 2D/3D graph SLAM, submaps + branch-and-bound loop closure. Excellent maps; notoriously fiddly to tune; maintenance has slowed.
  • LOAM → LIO-SAM → FAST-LIO2 lineage — 3D lidar(-inertial) odometry. LOAM split feature extraction (edges/planes) from mapping; LIO-SAM put lidar+IMU in a factor graph; FAST-LIO2 does direct point-to-map registration with a tightly-coupled iterated EKF and an incremental KD-tree — fast, robust, the current default answer, and already in dimos as a native module.

Visual:

  • ORB-SLAM3 — feature-based (ORB corners), keyframes, DBoW place recognition, full loop closing, mono/stereo/RGB-D/VIO, multi-map. The academic reference implementation; production caveat: feature-poor walls, lighting sensitivity, and it's a complex research codebase.
  • DSO — direct sparse odometry: skips features, optimizes photometric error on pixel intensities. Sharp in good conditions; fragile to exposure/lighting changes; odometry only (no loop closure).
  • VINS-Mono / MSCKF — the VIO standards: sliding-window optimization (VINS) vs filtering (MSCKF, what runs on phones/drones where compute is tight). VINS-Mono is the usual "add VIO to a robot" starting point.
  • DROID-SLAM / DPVO — learned optical-flow-based SLAM/VO with differentiable bundle adjustment. Remarkably robust on benchmarks; needs desktop-class GPU (multi-GB VRAM), which is exactly why they're not on service robots yet.

Dimensional: know FAST-LIO2 (it's in their repo), ORB-SLAM3 and VINS-Mono (the "why not just run vSLAM?" conversation — answer: compute, robustness, scale ambiguity for mono, and integration cost vs a tag map), and DROID/DPVO (the "learned SLAM" question).

8. Fusion + robustness engineering — the FDE section

The standard fusion pattern (ROS robot_localization shape): an EKF/UKF ingests continuous sources (wheel/leg odom, IMU) at high rate and absolute fixes (AMCL, ICP, tags, GPS) at low rate; typically run as two instances — one publishing odom → base_link (continuous only), one publishing map → odom (with fixes). Each input contributes only the DOFs it actually measures, weighted by covariance.

Covariance gating — the one mechanism that separates production localization from demos. Before fusing a fix, compute its Mahalanobis distance against the current belief: d² = (z − ẑ)ᵀ S⁻¹ (z − ẑ). Gate at a χ² threshold (e.g. 99%): a fix that disagrees with the belief by more than its claimed uncertainty allows is rejected or down-weighted. This is what stops one glinting mis-detected tag from yanking the TF tree. Complementary tricks: require k consistent detections before trusting a tag after a gap; rate-limit correction magnitude; never publish a correction whose own reprojection error is high.

Health monitoring + fallback ladders. Each source exports liveness + quality (ICP fitness, tag reprojection error, feature count); a supervisor walks a degradation ladder. Yours, from the spec: (1) lidar+odom nominal → (2) lidar degraded, markers carry the correction → (3) marker-dense zone (elevator cab, dock), markers alone suffice → (4) everything on-board, zero network dependency. The engineering content of "graceful degradation" is that transitions are explicit, hysteretic, and logged — not emergent.

Known environment killers and their mitigations:

  • Glass & mirrors: lidar sees through glass or reflects — the map lies. Mirrors create phantom rooms. Mitigations: intensity filtering, mapping-time masking, camera-based sources (tags are immune — better: put a tag on the glass wall).
  • Long corridors: geometry constrains only 5 DOF; ICP/AMCL slide longitudinally. Mitigation: any along-corridor landmark — a door frame, or one tag every N meters.
  • Dynamic environments (people): crowds occlude and contradict the static map. Mitigations: filter dynamic points (raycast disagreement), weight the map's permanent structure, ceiling-facing sensors (Pudu's actual insight — nobody occludes the ceiling).
  • Elevators: the perfect storm — mirrored steel defeats lidar, cab acceleration corrupts IMU assumptions, GPS-denied, and the map literally moves. One tag inside the cab restores full 6-DOF absolute pose. This is your marquee story; own it.
  • Lifelong mapping / map updates: furniture moves; a frozen premap decays. Production answer: localize against the stable structure, log persistent disagreements, patch the map from routine traffic (or re-survey the changed zone) — never let the robot silently edit the map it's localizing against (self-licking feedback loop).

Dimensional: dimos has no fusion filter and no gating today — RelocalizationModule gates on ICP fitness and that's the extent of it. Even a covariance-gated, hysteretic tag corrector puts you ahead of the current stack's robustness story, and it's the part Stash's ">95% in production" bar is actually about.

9. Evaluation

  • ATE (absolute trajectory error): RMSE of estimated vs ground-truth positions after aligning the trajectories — measures global consistency, dominated by drift and loop-closure quality.
  • RPE (relative pose error): error of the estimated motion over fixed deltas (per meter / per second) — measures local odometry quality, independent of accumulated drift. Report both; they diagnose different subsystems.
  • Benchmarks you'll hear: KITTI (outdoor driving), EuRoC MAV (drone VIO, mocap ground truth), TUM RGB-D (indoor handheld). Good for calibrating expectations ("ORB-SLAM3 does cm-level ATE on EuRoC"), not for predicting deployment behavior.
  • Measuring ">95% reliable" on site, without a mocap rig — the FDE-grade question:
    1. Define the event: "fix accepted AND within X cm / Y° of truth, within Z m of a tag" — reliability of a primitive is per-opportunity, not per-hour.
    2. Ground truth options, cheap → gold: surveyed checkpoints on the floor (tape measure + laser rangefinder, robot visits repeatedly); a held-out tag as ground truth for the others; the lidar stack in a geometry-rich zone as reference (only valid where it's healthy); total station or mocap when someone pays for it.
    3. Then measure the distribution, not the mean: repeatability at a checkpoint (return N times, spread of reported poses), fix-acceptance rate vs distance/angle/lighting, time-to-relocalize after kidnap. Log everything; the histogram is the deliverable.

Dimensional: put exactly this protocol in your spec's acceptance section — it reads as "has done deployments" in a way no algorithm section can.

10. What production service robots really run

The boring winning stack, deployed ten-thousands of times over: map once at install → freeze → localize forever. Concretely: 2D lidar + wheel/leg odom + IMU, fused EKF, AMCL or ICP-against-premap for corrections, occupancy-grid costmaps, and an install/commissioning workflow — a tech joysticks the robot around, the map is built offline (with loop closure), a human QAs and annotates it (no-go zones, dock, POIs), and that artifact is the product. Re-survey is a paid site visit, which is why "map updates without re-survey" is a real commercial feature.

Pudu's ceiling approach: delivery robots in restaurants — dynamic, crowded, table layouts shift weekly. Their insight: the ceiling is the one static, never-occluded surface, so localize off an upward camera (historically ceiling-mounted markers; marketed as "PuduSLAM" visual localization; their numeric claims — 30 m ceilings, 15-min setup — trace only to their own PR, unverified). Your ceiling-vSLAM v2 idea is this, and your wall-tag v1 is the same philosophy with solved data association.

Warehouse AMR practice: the highest-reliability indoor localization in production is fiducial-based — Amazon/Kiva robots drive over a grid of floor QR codes; many AMR vendors use wall/rack tags or reflector beacons (the pre-camera version of the same idea). Nobody deploys pure learned SLAM: compute cost, no failure predictability, no way to hand a technician a map to annotate, and regressions are un-debuggable. Production wants auditable localization — "tag 23 by the elevator" beats "the network usually recognizes this corridor."

Dimensional: their pitch is deploy-like-software; the missing piece between "SDK" and "25k robots" is exactly this commissioning + reliability layer. A tag map is the most software-shaped localization artifact there is — a config file you can version, diff, and ship.

11. Mapping the concepts onto dimos

Concept (section)dimos todayPath
TF tree (§1)Yes — publish/get, 10 s buffer; world → base_link → camera_link → camera_opticaldimos/protocol/tf/tf.py
Odometry (§3)Go2 onboard legged odom via WebRTC → TF; FastLIO2/PointLIO native modules for Livoxdimos/robot/unitree/go2/connection.py; dimos/hardware/sensors/lidar/fastlio2/module.py
Map representation (§4)Voxel map from lidar (CUDA); costmap derived from height gradientsdimos/mapping/voxels.py; dimos/mapping/costmapper.py
Localization in known map (§5)ICP vs .pc2.lcm premap every 2 s, fitness-gated → TF world → map — the only localizer, and your structural templatedimos/mapping/relocalization/module.py
SLAM / loop closure (§6)Offline-only PGO at map build; no live loop closure (admitted drift)dimos/mapping/loop_closure/pgo.py; docs/capabilities/navigation/deep_dive.md
Fiducials (§5)Detection + PnP exist; direction is inverted (robot pose → marker poses in world); no robot-from-markers, no vSLAM anywheredimos/perception/fiducial/marker_pose.py, marker_detect.py
Fusion/gating (§8)None beyond ICP fitness gate — open field
Planner consuming pose (§1)A* planner takes a raw odom pose stream (TODO: use TF)dimos/navigation/replanning_a_star/module.py

Your VisualRelocalizationModule in this taxonomy: a landmark-map localizer (§5) publishing a map → odom-style TF correction (§1) with covariance-gated fusion (§8) against a tag map built by inverting their existing pipeline (§5 tag-map building) — i.e., it fills the two empty rows of this table, reusing the fiducial row's code.

12. Whiteboard drills

1. "Robot wakes up kidnapped — walk me through recovery." No prior → global localization. Particle filter: spread particles map-wide (10k+), weight against lidar, converge over motion; AMCL's injection handles the case where it thinks it knows but is wrong (likelihood collapse triggers random particles). With tags: one sighting = full 6-DOF absolute pose — global relocalization is a single frame, then hand back to tracking. Mention: never trust the first fix blindly — require consistency over k frames before snapping the TF.

2. "Why does monocular VO drift in scale but VIO doesn't?" A single camera measures bearings only; the reprojection objective is invariant to scaling the whole scene + translation, so scale is unobservable and random-walks. The IMU measures metric acceleration (m/s²); integrating it over the same interval pins the translation magnitude. Fused, camera fixes drift, IMU fixes scale + fast motion. (Tags solve it differently: known physical tag size makes PnP metric from one frame.)

3. "Your loop closure fired wrong. What happens, and how do you defend?" The optimizer redistributes a nonexistent error around the loop — the map folds onto itself, confidently. Defenses in layers: geometric verification before insertion (ICP the two scans, require high fitness); robust kernels (Huber/Cauchy) so no single edge dominates; switchable constraints so the back-end can disbelieve it; keep the raw graph so a human can excise the edge and re-optimize. Tags dodge the root cause: data association is solved by the broadcast ID (only failure is a duplicated/moved tag — govern the tag map).

4. "Design localization for a mirrored elevator lobby + cab." Enumerate why every default dies: mirrors give lidar phantom geometry; glass is invisible to it; cab motion violates IMU zero-velocity assumptions; the cab interior moves (its map is valid only in a cab-fixed frame). Answer: one tag inside the cab (cab-frame anchor — pose relative to the cab, which is what door-alignment needs), tags in the lobby (world-frame anchors on entry/exit), hysteretic frame handoff world↔cab keyed on door state/tag visibility, covariance-gated so lidar is ignored where it's known-bad. This is the graceful-degradation ladder instantiated.

5. "Why publish map→odom instead of setting the robot's pose?" Decoupling: controllers need continuity (odom chain never jumps), planners need accuracy (map chain absorbs jumps); localization can be slow/late/dead without stalling control, because corrections compose with whatever odometry accumulated since. Also isolates failure: a bad fix is one gateable transform, not a teleported robot.

6. "Robot's pose estimate oscillates between two spots in a long corridor. Diagnose." Perceptual aliasing — two poses explain the scan equally well; the belief is genuinely multimodal and a Gaussian-ish localizer snaps between modes. Fixes: representation that holds multimodality (particles) plus disambiguating evidence — any unique landmark breaks the symmetry: a door frame in the measurement model, or literally one sticker per corridor segment. Don't tune your way out of an observability problem.

7. "How do you demonstrate >95% reliability on site with no mocap?" Define the event per-opportunity (fix within X cm/Y° when within Z m of a tag); surveyed floor checkpoints + repeatability spread; a held-out tag as truth for the rest; log acceptance rate vs distance/angle/lighting and time-to-relocalize; ship the histogram. Bonus: the same logging is the health monitor in production.

8. "You build the tag map by driving around — but your odometry drifts. How is the map consistent?" Don't take each tag's first-seen pose at face value — that bakes drift in. Collect all camera-tag observations, build a factor graph (poses + tag poses as nodes; odom + PnP observations as edges), optimize jointly — re-observing tag A after a loop is a loop closure that squeezes drift out of the whole map (tagSLAM). Cheap version for the trial: anchor one tag as origin, chain multi-tag co-visibility, refine with PGO — reuse dimos/mapping/loop_closure/pgo.py concepts at map-build time, offline, exactly like their lidar premap flow.

9. "When does AMCL beat ICP-against-premap, and vice versa?" AMCL: multimodal belief, global init, kidnap recovery, cheap in 2D — but 2D, needs a good grid map, degrades with dynamic clutter. ICP-premap: full 3D, accurate when initialized, simple — but local-only (needs a guess), no kidnap recovery, lies confidently in aliased geometry. Production often runs ICP for tracking + something else (AMCL-style or tags) for initialization/recovery. dimos chose ICP-only, which is why kidnap/global-init is an open gap your module closes.

10. "Your PnP poses look right up close but swing wildly at 4–5 m. Why?" Depth from a planar tag comes from perspective foreshortening, which shrinks with distance — depth error grows ~d² while bearing stays good; plus the IPPE two-fold ambiguity flips near-frontal views at range. Mitigations: bigger tags or tag bundles, treat far tags as bearing-only (or inflate depth covariance accordingly), reject beyond a range threshold, joint PnP over multiple tags. If it's also wrong up close: recheck calibration — bad intrinsics/extrinsics bias PnP everywhere and mimic these symptoms.

13. Reading list

Book: Thrun, Burgard, Fox — Probabilistic Robotics. Read: ch. 2–3 (Bayes filter, Gaussians), 4 (particle filters), 5–6 (motion + measurement models), 7–8 (MCL, grid localization), 10–11 (SLAM, graph SLAM), 13 (FastSLAM). Skim the rest. (Barfoot's State Estimation for Robotics is the free, more mathematical alternative.)

Papers (canonical, in reading order):

  1. Durrant-Whyte & Bailey, "SLAM: Part I & II" (2006) — the field's framing, fast read.
  2. Dellaert et al., "Monte Carlo Localization" (1999) — MCL from the source.
  3. Grisetti et al., "A Tutorial on Graph-Based SLAM" (2010) — the back-end, gently.
  4. Olson, "AprilTag" (2011) + Wang & Olson, "AprilTag 2" (2016) — your primitive's foundations.
  5. Mur-Artal & Tardós, "ORB-SLAM2" (2017) — read for the architecture (tracking / local mapping / loop closing threads).
  6. Xu et al., "FAST-LIO2" (2022) — the modern lidar-inertial standard; it's in the dimos repo.
  7. Cadena et al., "Past, Present, and Future of SLAM: Toward the Robust-Perception Age" (2016) — the survey that organizes everything above.

Codebases to actually read:

  1. apriltag (AprilRobotics C library) + tagSLAM — your project's lineage: detection → PnP → tag-map factor graph. tagSLAM's docs are a mini-course in themselves.
  2. Nav2 AMCL (ROS 2) — a production-hardened particle filter, readable in an afternoon; steal its recovery + covariance conventions.
  3. dimos itself: dimos/mapping/relocalization/ (your template), dimos/perception/fiducial/ (your building blocks), dimos/mapping/loop_closure/pgo.py (map-build optimization).

The one-paragraph version you should be able to say cold: localization is a Bayes filter fusing fast-but-drifting dead reckoning with slow-but-absolute fixes, published as a map→odom correction over an odometry chain that never jumps; maps are whatever the fix-generator can score a pose against — grids for lidar, landmark maps for tags; SLAM builds that map once via graph optimization with loop closures, then production freezes it; and reliability comes not from the algorithm but from covariance-honest fusion, χ²-gating, explicit degradation ladders, and measuring the failure distribution on site. Your trial project is the smallest complete instance of every one of those ideas.