Skip to content

Commit bbd5980

Browse files
timholyhyrodium
andauthored
Use sinc to fix RotationVec near zero (#272)
* Use `sinc` to fix RotationVec near zero By converting straight from RotationVec->QuatRotation, and using an implementation that exploits `sinc` to handle θ ≈ 0, we get continuous & differentiable behavior. * Use new implementation only for small theta * Relax adjoint test to approxeq * Fix test with `@test_broken` * Update tests for `inv`, `adjoint`, and `transpose` * Fix type instability --------- Co-authored-by: Yuto Horikawa <hyrodium@gmail.com>
1 parent ca676d3 commit bbd5980

File tree

3 files changed

+45
-16
lines changed

3 files changed

+45
-16
lines changed

src/angleaxis_types.jl

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,24 +172,24 @@ function (::Type{RV})(aa::AngleAxis) where RV <: RotationVec
172172
return RV(aa.theta * aa.axis_x, aa.theta * aa.axis_y, aa.theta * aa.axis_z)
173173
end
174174

175-
function (::Type{Q})(rv::RotationVec) where Q <: QuatRotation
176-
return QuatRotation(AngleAxis(rv))
177-
end
178-
179-
(::Type{RV})(q::QuatRotation) where {RV <: RotationVec} = RV(AngleAxis(q))
180-
181-
function Base.:*(rv::RotationVec{T1}, v::StaticVector{3, T2}) where {T1,T2}
175+
@inline function (::Type{Q})(rv::RotationVec) where Q <: QuatRotation
182176
theta = rotation_angle(rv)
183-
if (theta > eps(T1)) # use eps here because we have the 1st order series expansion defined
184-
return AngleAxis(rv) * v
177+
if theta < sqrt(eps(typeof(theta)))
178+
ϕ = theta / π / 2
179+
sc = sinc(ϕ) / 2 # this form gracefully handles theta = 0
180+
qx, qy, qz = sc * rv.sx, sc * rv.sy, sc * rv.sz
181+
return Q(cos(theta / 2), qx, qy, qz, false)
185182
else
186-
return similar_type(typeof(v), promote_type(T1,T2))(
187-
v[1] + rv.sy * v[3] - rv.sz * v[2],
188-
v[2] + rv.sz * v[1] - rv.sx * v[3],
189-
v[3] + rv.sx * v[2] - rv.sy * v[1])
183+
s, c = sincos(theta / 2)
184+
= s / theta
185+
return Q(c, sθ * rv.sx, sθ * rv.sy, sθ * rv.sz, false)
190186
end
191187
end
192188

189+
(::Type{RV})(q::QuatRotation) where {RV <: RotationVec} = RV(AngleAxis(q))
190+
191+
Base.:*(rv::RotationVec, v::StaticVector{3}) = QuatRotation(rv) * v
192+
193193
@inline Base.:*(rv::RotationVec, r::Rotation) = QuatRotation(rv) * r
194194
@inline Base.:*(rv::RotationVec, r::RotMatrix) = QuatRotation(rv) * r
195195
@inline Base.:*(rv::RotationVec, r::MRP) = QuatRotation(rv) * r
@@ -203,7 +203,7 @@ end
203203
@inline Base.inv(rv::RotationVec) = RotationVec(-rv.sx, -rv.sy, -rv.sz)
204204

205205
# rotation properties
206-
@inline rotation_angle(rv::RotationVec) = sqrt(rv.sx * rv.sx + rv.sy * rv.sy + rv.sz * rv.sz)
206+
@inline rotation_angle(rv::RotationVec) = hypot(rv.sx, rv.sy, rv.sz)
207207
function rotation_axis(rv::RotationVec) # what should this return for theta = 0?
208208
theta = rotation_angle(rv)
209209
return (theta > 0 ? SVector(rv.sx / theta, rv.sy / theta, rv.sz / theta) : SVector(one(theta), zero(theta), zero(theta)))

test/derivative_tests.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,20 @@ using ForwardDiff
180180
@test FD_jac R_jac
181181
end
182182
end
183+
184+
@testset "RotationVec near zero" begin
185+
rot(x) = RotationVec(x, 0, 0)
186+
for i = 1:10
187+
v = randn(SVector{3,Float64})
188+
rotv(x) = rot(x)*v
189+
drotv(x) = ForwardDiff.derivative(rotv, x)
190+
# The following broken tests will be fixed by https://github.com/JuliaDiff/ForwardDiff.jl/pull/669
191+
@test_broken drotv(0.0) [0, -v[3], v[2]]
192+
@test drotv(1e-20) [0, -v[3], v[2]]
193+
@test_broken ForwardDiff.derivative(drotv, 0.0) [0, -v[2], -v[3]]
194+
@test ForwardDiff.derivative(drotv, 1e-20) [0, -v[2], -v[3]]
195+
end
196+
end
183197
#=
184198
# rotate a point by an MRP
185199
@testset "Jacobian (MRP rotation)" begin

test/rotation_tests.jl

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,7 @@ all_types = (RotMatrix3, RotMatrix{3}, AngleAxis, RotationVec,
154154
r1 = rand(R)
155155
r2 = rand(R)
156156
v = @SVector rand(3)
157-
@test inv(r1) == adjoint(r1)
158-
@test inv(r1) == transpose(r1)
157+
@test inv(r1) === adjoint(r1) === transpose(r1)
159158
@test inv(r1)*r1 I
160159
@test r1*inv(r1) I
161160
@test r1/r1 I
@@ -334,9 +333,25 @@ all_types = (RotMatrix3, RotMatrix{3}, AngleAxis, RotationVec,
334333
end
335334
@test norm(rotation_axis(QuatRotation(1.0, 0.0, 0.0, 0.0))) 1.0
336335

336+
337337
# TODO RotX, RotXY?
338338
end
339339

340+
@testset "RotationVec->QuatRotation, especially near zero" begin
341+
for i = 1:10
342+
p = rand(RotationVec)
343+
all(iszero, Rotations.params(p)) && continue
344+
@test QuatRotation(p) QuatRotation(AngleAxis(p))
345+
end
346+
347+
@test rotation_angle(RotationVec(0.0, 0.0, 0.0)) 0.0
348+
@test rotation_axis(RotationVec(0.0, 0.0, 0.0)) [1.0, 0.0, 0.0]
349+
@test QuatRotation(RotationVec(0.0, 0.0, 0.0)) == QuatRotation(1.0, 0.0, 0.0, 0.0)
350+
qr = QuatRotation(RotationVec(2e-30, 0.0, 0.0))
351+
@test qr.w 1
352+
@test qr.x 1e-30
353+
end
354+
340355
#########################################################################
341356
# Check construction of QuatRotation given two vectors
342357
#########################################################################

0 commit comments

Comments
 (0)