/*
* Copyright 2016 The Cartographer Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using CartographerSharp.Transform;
using System;
using RobotNet10.Shared.Numbers;
namespace CartographerSharp.Mapping;
///
/// Keeps track of the orientation using angular velocities and linear
/// accelerations from an IMU. Because averaged linear acceleration (assuming
/// slow movement) is a direct measurement of gravity, roll/pitch does not drift,
/// though yaw does.
///
public class ImuTracker
{
private readonly double _imuGravityTimeConstant;
private long _time;
private long _lastLinearAccelerationTime;
private Quaternion _orientation;
private Vector3 _gravityVector;
private Vector3 _imuAngularVelocity;
// Recovery tracking: count consecutive invalid gravity states
private int _invalidGravityCount;
private const int MaxInvalidGravityBeforeReset = 10; // Reset after 10 consecutive invalid states
public ImuTracker(double imuGravityTimeConstant, long time)
{
_imuGravityTimeConstant = imuGravityTimeConstant;
_time = time;
_lastLinearAccelerationTime = long.MinValue;
_orientation = Quaternion.Identity;
_gravityVector = Vector3.UnitZ;
_imuAngularVelocity = Vector3.Zero;
_invalidGravityCount = 0;
}
///
/// Copy constructor.
///
public ImuTracker(ImuTracker other)
{
_imuGravityTimeConstant = other._imuGravityTimeConstant;
_time = other._time;
_lastLinearAccelerationTime = other._lastLinearAccelerationTime;
_orientation = other._orientation;
_gravityVector = other._gravityVector;
_imuAngularVelocity = other._imuAngularVelocity;
_invalidGravityCount = other._invalidGravityCount;
}
///
/// Advances to the given 'time' and updates the orientation to reflect this.
///
public void Advance(long time)
{
if (time < _time)
{
// DEBUG: Log detailed timestamp information
var timeDiffMs = (_time - time) / TimeSpan.TicksPerMillisecond;
// If the difference is small (< 100ms), it's likely due to synchronization issues
// between multiple sensors or old scans being reprocessed. In this case, we advance
// to current time instead of throwing. This is a workaround for RangeDataCollator
// synchronization issues and old scan reprocessing.
if (timeDiffMs >= 100)
{
// For larger differences, throw exception as it indicates a real problem
throw new ArgumentException($"time ({time}) must be >= current time ({_time}), diff={timeDiffMs}ms", nameof(time));
}
}
var deltaT = (time - _time) / 10_000_000.0; // Convert ticks to seconds (10 million ticks per second)
var rotation = TransformOperations.AngleAxisVectorToRotationQuaternion(
_imuAngularVelocity * deltaT);
_orientation = Quaternion.Normalize(_orientation * rotation);
// Rotate gravity vector by inverse rotation (conjugate)
// In C++: gravity_vector_ = rotation.conjugate() * gravity_vector_
// In C#: equivalent to Transform with conjugate quaternion
var rotationConjugate = Quaternion.Conjugate(rotation);
_gravityVector = Vector3.Transform(_gravityVector, rotationConjugate);
_time = time;
}
///
/// Updates from an IMU reading (in the IMU frame).
///
public void AddImuLinearAccelerationObservation(Vector3 imuLinearAcceleration)
{
// Validate input: reject zero or near-zero acceleration (invalid sensor data)
var inputMagnitude = imuLinearAcceleration.Length();
if (inputMagnitude < 0.1) // Less than 0.1 m/s² is invalid (should be ~9.81 when stationary)
{
// Skip invalid reading, don't update state
return;
}
// Update the 'gravity_vector_' with an exponential moving average using the
// 'imu_gravity_time_constant'.
var deltaT = _lastLinearAccelerationTime > long.MinValue
? (_time - _lastLinearAccelerationTime) / 10_000_000.0 // Convert ticks to seconds (10 million ticks per second)
: double.PositiveInfinity;
_lastLinearAccelerationTime = _time;
var alpha = 1.0 - Math.Exp(-deltaT / _imuGravityTimeConstant);
_gravityVector = (1.0 - alpha) * _gravityVector + alpha * imuLinearAcceleration;
// Change the 'orientation_' so that it agrees with the current 'gravity_vector_'.
// Match C++: FromTwoVectors(gravity_vector_, orientation_.conjugate() * Eigen::Vector3d::UnitZ())
// This computes rotation from gravity_vector_ (in IMU frame) to UnitZ transformed to IMU frame
var unitZInImuFrame = Vector3.Transform(Vector3.UnitZ, Quaternion.Inverse(_orientation));
var rotation = FromTwoVectors(_gravityVector, unitZInImuFrame);
_orientation = Quaternion.Normalize(_orientation * rotation);
// Validate: gravity vector transformed by orientation should point up (positive Z in world frame)
// When IMU is level, gravity in world frame should be (0, 0, +g) after transform
var transformedGravity = Vector3.Transform(_gravityVector, _orientation);
var normalizedTransformedGravity = Vector3.Normalize(transformedGravity);
if (transformedGravity.Z <= 0 || normalizedTransformedGravity.Z < 0.9)
{
_invalidGravityCount++;
// Recovery: if too many consecutive invalid states, reset to known good state
if (_invalidGravityCount >= MaxInvalidGravityBeforeReset)
{
Console.WriteLine($"[ImuTracker] RECOVERY: Resetting after {_invalidGravityCount} consecutive invalid gravity states. " +
$"TransformedGravity=({transformedGravity.X:F3},{transformedGravity.Y:F3},{transformedGravity.Z:F3})");
// Reset gravity vector to point in the direction of current acceleration
// (assuming robot is mostly stationary, acceleration ≈ gravity)
_gravityVector = Vector3.Normalize(imuLinearAcceleration) * 9.81;
// Reset orientation to align gravity with world Z-axis
var gravityDirection = Vector3.Normalize(_gravityVector);
_orientation = FromTwoVectors(gravityDirection, Vector3.UnitZ);
_invalidGravityCount = 0;
}
}
else
{
// Valid state - reset counter
_invalidGravityCount = 0;
}
}
///
/// Updates from an IMU reading (in the IMU frame).
///
public void AddImuAngularVelocityObservation(Vector3 imuAngularVelocity)
{
_imuAngularVelocity = imuAngularVelocity;
}
///
/// Query the current time.
///
public long Time => _time;
///
/// Query the current orientation estimate.
///
public Quaternion Orientation => _orientation;
///
/// Computes a quaternion that rotates vector 'a' to vector 'b'.
/// Equivalent to Eigen::Quaterniond::FromTwoVectors().
///
private static Quaternion FromTwoVectors(Vector3 a, Vector3 b)
{
// Normalize input vectors
a = Vector3.Normalize(a);
b = Vector3.Normalize(b);
// If vectors are parallel, return identity
var dot = Vector3.Dot(a, b);
if (Math.Abs(dot - 1.0) < 1e-6)
{
return Quaternion.Identity;
}
// If vectors are opposite, need special handling
if (Math.Abs(dot + 1.0) < 1e-6)
{
// Find an orthogonal vector to 'a'
Vector3 orthogonal;
if (Math.Abs(a.X) < Math.Abs(a.Y))
{
orthogonal = Vector3.UnitX;
}
else
{
orthogonal = Vector3.UnitY;
}
orthogonal = Vector3.Normalize(Vector3.Cross(a, orthogonal));
// Create 180-degree rotation around orthogonal axis
return Quaternion.CreateFromAxisAngle(orthogonal, Math.PI);
}
// General case: compute rotation axis and angle
var axis = Vector3.Cross(a, b);
var axisLength = axis.Length();
if (axisLength < 1e-6)
{
return Quaternion.Identity;
}
axis = Vector3.Normalize(axis);
var angle = Math.Acos(Math.Clamp(dot, -1.0, 1.0));
return Quaternion.CreateFromAxisAngle(axis, angle);
}
}