Skip to content

Numerically vulnerable axis calculation in Rotation3 #1382

@IshitaTakeshi

Description

@IshitaTakeshi

Abstract

nalgebra::Rotation3::axis relies on numerically vulnerable calculation.
It may return None in the place where it should return a non-None value.

Issue

In the current implementation of Rotation3, the rotation axis is calculated in the following way.

    #[inline]
    #[must_use]
    pub fn axis(&self) -> Option<Unit<Vector3<T>>>
    where
        T: RealField,
    {
        let rotmat = self.matrix();
        let axis = SVector::<T, 3>::new(
            rotmat[(2, 1)].clone() - rotmat[(1, 2)].clone(),
            rotmat[(0, 2)].clone() - rotmat[(2, 0)].clone(),
            rotmat[(1, 0)].clone() - rotmat[(0, 1)].clone(),
        );

        Unit::try_new(axis, T::default_epsilon())
    }

If I initialize Rotation3 with a rotation of π radians around the y-axis, the code will look like this.

use nalgebra::{Rotation3, Vector3};

const PI: f64 = std::f64::consts::PI;

fn main() {
    let r = Rotation3::from_axis_angle(&Vector3::y_axis(), PI);
    println!("axis = {:?}", r.axis());
    println!("axis_angle = {:?}", r.axis_angle());
}

And it actually works.

axis = Some([[0.0, 1.0, 0.0]])
axis_angle = Some(([[0.0, 1.0, 0.0]], 3.141592653589793))

However, this is very vulnerable, because this behavior relies on the rounding error of the internal matrix representation.

I added the line to print the axis value in the axis function.

    #[inline]
    #[must_use]
    pub fn axis(&self) -> Option<Unit<Vector3<T>>>
    where  
        T: RealField,
    {
        let rotmat = self.matrix();
        let axis = SVector::<T, 3>::new(
            rotmat[(2, 1)].clone() - rotmat[(1, 2)].clone(),
            rotmat[(0, 2)].clone() - rotmat[(2, 0)].clone(),
            rotmat[(1, 0)].clone() - rotmat[(0, 1)].clone(),
        );
          
        println!("intelnal axis value = {:?}", axis);
        Unit::try_new(axis, T::default_epsilon())    
    }

And it said

intelnal axis value = [[0.0, 2.4492935982947064e-16, 0.0]]

This is very close to [0, 0, 0]. If there was no rounding error, this code would return None in the place where it should return [0, 1, 0].

Why this happens

The matrix corresponding to the rotation of π radians around the y-axis is, without rounding error,

-1  0  0
 0  1  0
 0  0 -1

So if we calculate axis based on the matrix above, it becomes [0, 0, 0].
The same happens for the rotation around the x-axis, z-axis, or rotation of π radians around any axis. Actually, on my Ubuntu desktop, the code below printed None.

use nalgebra::{Rotation3, Vector3, Unit};

const PI: f64 = std::f64::consts::PI;

fn main() {
    let x = Unit::new_normalize(Vector3::new(1., 2., 1.));
    let r = Rotation3::from_axis_angle(&x, PI);
    println!("axis = {:?}", r.axis());
}

Possible solution

One possible solution is to handle these singular value cases as explained in this paper, section 9.4.1.2 "Logarithm map".
A tutorial on SE(3) transformation parameterizations and on-manifold optimization

Another solution is to replace the internal rotation representation in Rotation3 from a 3x3 matrix to a unit quaternion.
The conversion from a unit quaternion to a rotation vector is described in the same section of the paper.
This will improve the overall performance including rotation multiplication, but requires a large amount of modifications.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions