Skip to content

Commit eb9aad3

Browse files
BUG: fix dual quaternion normalization
1 parent 36b86c5 commit eb9aad3

File tree

6 files changed

+88
-67
lines changed

6 files changed

+88
-67
lines changed

doc/source/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ Dual Quaternion
421421

422422
~dual_quaternion_requires_renormalization
423423
~norm_dual_quaternion
424+
~dual_quaternion_squared_norm
424425

425426
~assert_unit_dual_quaternion
426427
~assert_unit_dual_quaternion_equal

pytransform3d/rotations/_quaternion.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -395,9 +395,6 @@ def q_prod_vector(q, v):
395395
def q_conj(q):
396396
r"""Conjugate of quaternion.
397397
398-
The conjugate of a unit quaternion inverts the rotation represented by
399-
this unit quaternion.
400-
401398
The conjugate of a quaternion :math:`\boldsymbol{q}` is often denoted as
402399
:math:`\boldsymbol{q}^*`. For a quaternion :math:`\boldsymbol{q} = w
403400
+ x \boldsymbol{i} + y \boldsymbol{j} + z \boldsymbol{k}` it is defined as
@@ -407,10 +404,13 @@ def q_conj(q):
407404
\boldsymbol{q}^* = w - x \boldsymbol{i} - y \boldsymbol{j}
408405
- z \boldsymbol{k}.
409406
407+
The conjugate of a unit quaternion inverts the rotation represented by
408+
this unit quaternion.
409+
410410
Parameters
411411
----------
412412
q : array-like, shape (4,)
413-
Unit quaternion to represent rotation: (w, x, y, z)
413+
Quaternion: (w, x, y, z)
414414
415415
Returns
416416
-------
@@ -421,7 +421,7 @@ def q_conj(q):
421421
--------
422422
rotor_reverse : Reverse of a rotor, which is the same operation.
423423
"""
424-
q = check_quaternion(q)
424+
q = check_quaternion(q, unit=False)
425425
return np.array([q[0], -q[1], -q[2], -q[3]])
426426

427427

pytransform3d/transformations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from ._dual_quaternion import (
5656
dual_quaternion_requires_renormalization,
5757
norm_dual_quaternion,
58+
dual_quaternion_squared_norm,
5859
check_dual_quaternion,
5960
assert_unit_dual_quaternion_equal,
6061
assert_unit_dual_quaternion,
@@ -136,6 +137,7 @@
136137
"random_exponential_coordinates",
137138
"pq_slerp",
138139
"norm_dual_quaternion",
140+
"dual_quaternion_squared_norm",
139141
"dual_quaternion_double",
140142
"dq_q_conj",
141143
"dq_conj",

pytransform3d/transformations/_dual_quaternion.py

Lines changed: 55 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -75,30 +75,37 @@ def check_dual_quaternion(dq, unit=True):
7575
return dq
7676

7777

78-
def dual_quaternion_requires_renormalization(dq, tolerance=1e-6):
79-
r"""Check if dual quaternion requires renormalization.
78+
def dual_quaternion_squared_norm(dq):
79+
"""Compute squared norm of dual quaternion.
8080
81-
Dual quaternions that represent transformations in 3D should have unit
82-
norm. Since the real and the dual quaternion are orthogonal, their dot
83-
product should be 0. In addition, :math:`\epsilon^2 = 0`. Hence,
81+
Parameters
82+
----------
83+
dq : array-like, shape (8)
84+
Dual quaternion to represent transform:
85+
(pw, px, py, pz, qw, qx, qy, qz)
8486
85-
.. math::
87+
Returns
88+
-------
89+
squared_norm : array, shape (2,)
90+
Squared norm of dual quaternion, which is a dual number with a real and
91+
a dual part.
92+
"""
93+
dq = np.asarray(dq)
94+
prod = concatenate_dual_quaternions(
95+
dq, dq_q_conj(dq, unit=False), unit=False
96+
)
97+
return prod[[0, 4]]
8698

87-
||\boldsymbol{p} + \epsilon \boldsymbol{q}||
88-
=
89-
\sqrt{(\boldsymbol{p} + \epsilon \boldsymbol{q}) \cdot
90-
(\boldsymbol{p} + \epsilon \boldsymbol{q})}
91-
=
92-
\sqrt{\boldsymbol{p}\cdot\boldsymbol{p}
93-
+ 2\epsilon \boldsymbol{p}\cdot\boldsymbol{q}
94-
+ \epsilon^2 \boldsymbol{q}\cdot\boldsymbol{q}}
95-
=
96-
\sqrt{\boldsymbol{p}\cdot\boldsymbol{p}},
9799

98-
i.e., the norm only depends on the real quaternion.
100+
def dual_quaternion_requires_renormalization(dq, tolerance=1e-6):
101+
r"""Check if dual quaternion requires renormalization.
102+
103+
Dual quaternions that represent transformations in 3D should have unit
104+
norm (:math:`1 + 0 \epsilon`), that is the real quaternion must have unit
105+
norm and the real and the dual quaternion must be orthogonal (their dot
106+
product should be 0).
99107
100-
This function checks unit norm and orthogonality of the real and dual
101-
part.
108+
This function checks unit norm and orthogonality of the real and dual part.
102109
103110
Parameters
104111
----------
@@ -127,11 +134,11 @@ def dual_quaternion_requires_renormalization(dq, tolerance=1e-6):
127134
assert_unit_dual_quaternion
128135
Checks unit norm and orthogonality of real and dual quaternion.
129136
"""
130-
real = dq[:4]
131-
dual = dq[4:]
132-
real_norm = np.linalg.norm(real)
133-
real_dual_dot = np.dot(real, dual)
134-
return abs(real_norm - 1.0) > tolerance or abs(real_dual_dot) > tolerance
137+
squared_norm = dual_quaternion_squared_norm(dq)
138+
return (
139+
abs(squared_norm[0] - 1.0) > tolerance
140+
or abs(squared_norm[1]) > tolerance
141+
)
135142

136143

137144
def norm_dual_quaternion(dq):
@@ -169,30 +176,12 @@ def norm_dual_quaternion(dq):
169176
.. [1] enki (2023). Properly normalizing a dual quaternion.
170177
https://stackoverflow.com/a/76313524
171178
"""
172-
dq = check_dual_quaternion(dq, unit=False)
173-
dq_prod = concatenate_dual_quaternions(dq, dq_q_conj(dq), unit=False)
174-
175-
prod_real = dq_prod[:4]
176-
prod_dual = dq_prod[4:]
177-
178-
real = np.copy(dq[:4])
179+
# 1. ensure unit norm of real quaternion
180+
dq = check_dual_quaternion(dq, unit=True)
181+
# 2. ensure orthogonality of real and dual quaternion
182+
real = dq[:4]
179183
dual = dq[4:]
180-
181-
prod_real_norm = np.linalg.norm(prod_real)
182-
if prod_real_norm == 0.0:
183-
real = np.array([1.0, 0.0, 0.0, 0.0])
184-
prod_real_norm = 1.0
185-
valid_dq = np.hstack((real, dual))
186-
prod_dual = concatenate_dual_quaternions(
187-
valid_dq, dq_q_conj(valid_dq), unit=False
188-
)[4:]
189-
190-
real_inv_sqrt = 1.0 / prod_real_norm
191-
dual_inv_sqrt = -0.5 * prod_dual * real_inv_sqrt**3
192-
193-
real = real_inv_sqrt * real
194-
dual = real_inv_sqrt * dual + concatenate_quaternions(dual_inv_sqrt, real)
195-
184+
dual = dual - np.dot(real, dual) * real
196185
return np.hstack((real, dual))
197186

198187

@@ -229,14 +218,9 @@ def assert_unit_dual_quaternion(dq, *args, **kwargs):
229218
Normalization that enforces unit norm and orthogonality of the real and
230219
dual quaternion.
231220
"""
232-
real = dq[:4]
233-
dual = dq[4:]
234-
235-
real_norm = np.linalg.norm(real)
236-
assert_array_almost_equal(real_norm, 1.0, *args, **kwargs)
237-
238-
real_dual_dot = np.dot(real, dual)
239-
assert_array_almost_equal(real_dual_dot, 0.0, *args, **kwargs)
221+
real_sq_norm, dual_sq_norm = dual_quaternion_squared_norm(dq)
222+
assert_array_almost_equal(real_sq_norm, 1.0, *args, **kwargs)
223+
assert_array_almost_equal(dual_sq_norm, 0.0, *args, **kwargs)
240224

241225
# The two previous checks are consequences of the unit norm requirement.
242226
# The norm of a dual quaternion is defined as the product of a dual
@@ -307,7 +291,7 @@ def dual_quaternion_double(dq):
307291
return -check_dual_quaternion(dq, unit=True)
308292

309293

310-
def dq_conj(dq):
294+
def dq_conj(dq, unit=True):
311295
"""Conjugate of dual quaternion.
312296
313297
There are three different conjugates for dual quaternions. The one that we
@@ -321,6 +305,12 @@ def dq_conj(dq):
321305
Unit dual quaternion to represent transform:
322306
(pw, px, py, pz, qw, qx, qy, qz)
323307
308+
unit : bool, optional (default: True)
309+
Normalize the dual quaternion so that it is a unit dual quaternion.
310+
A unit dual quaternion has the properties
311+
:math:`p_w^2 + p_x^2 + p_y^2 + p_z^2 = 1` and
312+
:math:`p_w q_w + p_x q_x + p_y q_y + p_z q_z = 0`.
313+
324314
Returns
325315
-------
326316
dq_conjugate : array, shape (8,)
@@ -331,11 +321,11 @@ def dq_conj(dq):
331321
dq_q_conj
332322
Quaternion conjugate of dual quaternion.
333323
"""
334-
dq = check_dual_quaternion(dq)
324+
dq = check_dual_quaternion(dq, unit=unit)
335325
return np.r_[dq[0], -dq[1:5], dq[5:]]
336326

337327

338-
def dq_q_conj(dq):
328+
def dq_q_conj(dq, unit=True):
339329
"""Quaternion conjugate of dual quaternion.
340330
341331
For unit dual quaternions that represent transformations, this function
@@ -352,6 +342,12 @@ def dq_q_conj(dq):
352342
Unit dual quaternion to represent transform:
353343
(pw, px, py, pz, qw, qx, qy, qz)
354344
345+
unit : bool, optional (default: True)
346+
Normalize the dual quaternion so that it is a unit dual quaternion.
347+
A unit dual quaternion has the properties
348+
:math:`p_w^2 + p_x^2 + p_y^2 + p_z^2 = 1` and
349+
:math:`p_w q_w + p_x q_x + p_y q_y + p_z q_z = 0`.
350+
355351
Returns
356352
-------
357353
dq_q_conjugate : array, shape (8,)
@@ -362,7 +358,7 @@ def dq_q_conj(dq):
362358
dq_conj
363359
Conjugate of a dual quaternion.
364360
"""
365-
dq = check_dual_quaternion(dq)
361+
dq = check_dual_quaternion(dq, unit=unit)
366362
return np.r_[dq[0], -dq[1:4], dq[4], -dq[5:]]
367363

368364

pytransform3d/transformations/_dual_quaternion.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import numpy.typing as npt
44
def check_dual_quaternion(
55
dq: npt.ArrayLike, unit: bool = ...
66
) -> np.ndarray: ...
7+
def dual_quaternion_squared_norm(dq: npt.ArrayLike) -> np.ndarray: ...
78
def dual_quaternion_requires_renormalization(
89
dq: npt.ArrayLike, tolerance: float = ...
910
) -> bool: ...
@@ -13,8 +14,8 @@ def assert_unit_dual_quaternion_equal(
1314
dq1: npt.ArrayLike, dq2: npt.ArrayLike, *args, **kwargs
1415
): ...
1516
def dual_quaternion_double(dq: npt.ArrayLike) -> np.ndarray: ...
16-
def dq_conj(dq: npt.ArrayLike) -> np.ndarray: ...
17-
def dq_q_conj(dq: npt.ArrayLike) -> np.ndarray: ...
17+
def dq_conj(dq: npt.ArrayLike, unit: bool = ...) -> np.ndarray: ...
18+
def dq_q_conj(dq: npt.ArrayLike, unit: bool = ...) -> np.ndarray: ...
1819
def concatenate_dual_quaternions(
1920
dq1: npt.ArrayLike, dq2: npt.ArrayLike, unit: bool = ...
2021
) -> np.ndarray: ...

pytransform3d/transformations/test/test_dual_quaternion.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,27 @@ def test_check_dual_quaternion():
4040

4141
def test_normalize_dual_quaternion():
4242
dq = [1, 0, 0, 0, 0, 0, 0, 0]
43+
norm = pt.dual_quaternion_squared_norm(dq)
44+
assert_array_almost_equal(norm, [1, 0])
45+
assert not pt.dual_quaternion_requires_renormalization(dq)
4346
dq_norm = pt.check_dual_quaternion(dq)
4447
pt.assert_unit_dual_quaternion(dq_norm)
4548
assert_array_almost_equal(dq, dq_norm)
4649
assert_array_almost_equal(dq, pt.norm_dual_quaternion(dq))
4750

4851
dq = [0, 0, 0, 0, 0, 0, 0, 0]
52+
norm = pt.dual_quaternion_squared_norm(dq)
53+
assert_array_almost_equal(norm, [0, 0])
54+
assert pt.dual_quaternion_requires_renormalization(dq)
4955
dq_norm = pt.check_dual_quaternion(dq)
5056
pt.assert_unit_dual_quaternion(dq_norm)
5157
assert_array_almost_equal([1, 0, 0, 0, 0, 0, 0, 0], dq_norm)
5258
assert_array_almost_equal(dq_norm, pt.norm_dual_quaternion(dq))
5359

5460
dq = [0, 0, 0, 0, 0.3, 0.5, 0, 0.2]
61+
norm = pt.dual_quaternion_squared_norm(dq)
62+
assert_array_almost_equal(norm, [0, 0])
63+
assert pt.dual_quaternion_requires_renormalization(dq)
5564
dq_norm = pt.check_dual_quaternion(dq)
5665
assert pt.dual_quaternion_requires_renormalization(dq_norm)
5766
assert_array_almost_equal([1, 0, 0, 0, 0.3, 0.5, 0, 0.2], dq_norm)
@@ -60,7 +69,7 @@ def test_normalize_dual_quaternion():
6069
)
6170

6271
rng = np.random.default_rng(999)
63-
for _ in range(5): # norm != 1
72+
for _ in range(5): # real norm != 1
6473
A2B = pt.random_transform(rng)
6574
dq = rng.standard_normal() * pt.dual_quaternion_from_transform(A2B)
6675
dq_norm = pt.check_dual_quaternion(dq)
@@ -75,9 +84,21 @@ def test_normalize_dual_quaternion():
7584
assert pt.dual_quaternion_requires_renormalization(dq_roundoff_error)
7685
dq_norm = pt.norm_dual_quaternion(dq_roundoff_error)
7786
pt.assert_unit_dual_quaternion(dq_norm)
87+
assert_array_almost_equal(
88+
pt.dual_quaternion_squared_norm(dq_norm), [1, 0]
89+
)
7890
assert not pt.dual_quaternion_requires_renormalization(dq_norm)
7991
assert_array_almost_equal(dq, dq_norm, decimal=3)
8092

93+
for _ in range(50):
94+
dq = rng.standard_normal(size=8)
95+
dq_norm = pt.norm_dual_quaternion(dq)
96+
assert_array_almost_equal(
97+
pt.dual_quaternion_squared_norm(dq_norm), [1, 0]
98+
)
99+
assert not pt.dual_quaternion_requires_renormalization(dq_norm)
100+
pt.assert_unit_dual_quaternion(dq_norm)
101+
81102

82103
def test_dual_quaternion_double():
83104
rng = np.random.default_rng(4183)

0 commit comments

Comments
 (0)