Skip to content

Commit c6516a1

Browse files
committed
Add more unit test cases
1 parent 061844f commit c6516a1

2 files changed

Lines changed: 98 additions & 5 deletions

File tree

mola_state_estimation_simple/src/StateEstimationSimple.cpp

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,13 @@ void StateEstimationSimple::fuse_odometry_3d_pose(
142142
return;
143143
}
144144

145-
const auto delta = sensedPose.mean - src.last_pose->mean;
146-
state_.last_pose->mean = state_.last_pose->mean + delta;
147-
state_.pose_already_updated_with_odom = true;
145+
const auto delta = sensedPose.mean - src.last_pose->mean;
146+
state_.last_pose->mean = state_.last_pose->mean + delta;
147+
// pose_already_updated_with_odom is NOT set here because
148+
// last_pose_obs_tim is updated to obs.timestamp below, so
149+
// estimated_navstate() will compute the correct dt and extrapolate
150+
// normally. (Contrast with fuse_odometry() which does NOT update
151+
// last_pose_obs_tim and must suppress extrapolation via the flag.)
148152

149153
// Derive twist from the per-source consecutive 3D odom poses.
150154
// This gives the correct wheel-odometry velocity independently of how
@@ -178,8 +182,17 @@ void StateEstimationSimple::fuse_odometry_3d_pose(
178182

179183
src.last_pose = sensedPose;
180184
src.last_obs_tim = obs.timestamp;
181-
// last_pose_obs_tim is intentionally NOT updated: it belongs to the LiDAR
182-
// ICP source and governs the validity window in estimated_navstate().
185+
186+
// Bootstrap last_pose when no SLAM source has set it yet, so that
187+
// estimated_navstate() can return valid results when CObservationRobotPose
188+
// is the sole pose source (e.g., in tests or lidar-odom-only setups).
189+
// When fuse_pose() is also active it owns last_pose_obs_tim and overwrites
190+
// it using the per-source src.last_obs_tim guard, so this update is safe.
191+
if (!state_.last_pose.has_value())
192+
{
193+
state_.last_pose = sensedPose;
194+
}
195+
state_.last_pose_obs_tim = obs.timestamp;
183196

184197
MRPT_LOG_DEBUG_STREAM("fuse_odometry_3d_pose('" << odomName << "'): pose=" << sensedPose.mean);
185198
}

mola_state_estimation_simple/tests/test-state-estimation-simple.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,85 @@ void test_robot_pose_observation()
343343
std::cout << "OK\n";
344344
}
345345

346+
// --------------------------------------------------------------------------
347+
// Test 7: Mixed ICP + 3-D wheel-odometry source
348+
// --------------------------------------------------------------------------
349+
// LiDAR ICP supplies the ground-truth trajectory via fuse_pose() at 1 Hz.
350+
// Wheel odometry arrives as CObservationRobotPose at 2 Hz and advances the
351+
// state between ICP scans via fuse_odometry_3d_pose().
352+
//
353+
// Key invariants checked:
354+
// (a) The twist derived from two consecutive ICP poses is the true ICP-to-ICP
355+
// velocity and is NOT contaminated by odometry deltas applied to
356+
// state_.last_pose between those scans.
357+
// (b) estimated_navstate() extrapolates correctly from the odom-advanced pose
358+
// while an ICP scan is still pending.
359+
// (c) estimated_navstate() extrapolates correctly from the ICP pose once a
360+
// new scan has been fused.
361+
void test_icp_and_3d_odometry_fusion()
362+
{
363+
std::cout << "[Test 7] ICP + 3-D odometry fusion... ";
364+
365+
mola::state_estimation_simple::StateEstimationSimple estimator;
366+
estimator.setMinLoggingLevel(mrpt::system::LVL_DEBUG);
367+
estimator.initialize(get_default_config());
368+
369+
const auto cov = mrpt::math::CMatrixDouble66::Identity();
370+
const double v_x = 1.0; // m/s — robot moves steadily in +X
371+
372+
// Helper: inject a CObservationRobotPose for the wheel-odometry source.
373+
auto send_wheel_odom = [&](double t, double x)
374+
{
375+
auto obs = mrpt::obs::CObservationRobotPose::Create();
376+
obs->timestamp = mrpt::Clock::fromDouble(t);
377+
obs->sensorLabel = "wheel_odom";
378+
obs->pose = mrpt::poses::CPose3DPDFGaussian(mrpt::poses::CPose3D(x, 0, 0), cov);
379+
estimator.onNewObservation(obs);
380+
};
381+
382+
// --- t=0: both sources initialise at origin ---
383+
estimator.fuse_pose(
384+
mrpt::Clock::fromDouble(0.0),
385+
mrpt::poses::CPose3DPDFGaussian(mrpt::poses::CPose3D::Identity(), cov), "map");
386+
send_wheel_odom(0.0, 0.0);
387+
388+
// --- t=0.5: odom fires mid-scan, advancing state_.last_pose to x=0.5 ---
389+
send_wheel_odom(0.5, 0.5);
390+
391+
// (b) Mid-scan extrapolation: odom twist (1 m/s) from (0.5,0,0) at t=0.5
392+
// queried at t=0.7 → expected x = 0.5 + 1.0*0.2 = 0.7
393+
{
394+
auto s = estimator.estimated_navstate(mrpt::Clock::fromDouble(0.7), "map");
395+
ASSERT_(s.has_value());
396+
ASSERT_NEAR_(s->pose.mean.x(), 0.7, 1e-3);
397+
}
398+
399+
// --- t=1: LiDAR ICP delivers the true vehicle position (1,0,0) ---
400+
estimator.fuse_pose(
401+
mrpt::Clock::fromDouble(1.0),
402+
mrpt::poses::CPose3DPDFGaussian(mrpt::poses::CPose3D(v_x, 0, 0), cov), "map");
403+
404+
// (a) Twist must equal the ICP-to-ICP velocity (1.0 m/s).
405+
// A regression would give 0.5 m/s because the broken code used the
406+
// odom-advanced state_.last_pose=(0.5,0,0) as the base instead of the
407+
// per-source ICP pose=(0,0,0).
408+
{
409+
auto tw = estimator.get_last_twist();
410+
ASSERT_(tw.has_value());
411+
ASSERT_NEAR_(tw->vx, v_x, 1e-3);
412+
}
413+
414+
// (c) Post-ICP extrapolation: ICP pose (1,0,0) at t=1, query at t=1.5
415+
// → expected x = 1.0 + 1.0*0.5 = 1.5
416+
{
417+
auto s = estimator.estimated_navstate(mrpt::Clock::fromDouble(1.5), "map");
418+
ASSERT_(s.has_value());
419+
ASSERT_NEAR_(s->pose.mean.x(), 1.5, 1e-3);
420+
}
421+
422+
std::cout << "OK\n";
423+
}
424+
346425
} // namespace
347426

348427
int main([[maybe_unused]] int argc, [[maybe_unused]] char** argv)
@@ -355,6 +434,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char** argv)
355434
test_planar_motion();
356435
test_gnss_ignored();
357436
test_robot_pose_observation();
437+
test_icp_and_3d_odometry_fusion();
358438

359439
std::cout << "\nAll StateEstimationSimple tests passed!\n";
360440
return 0;

0 commit comments

Comments
 (0)