Skip to content

Commit 77fdda1

Browse files
committed
fix quaternion convention
1 parent 909562e commit 77fdda1

2 files changed

Lines changed: 54 additions & 40 deletions

File tree

src/gncpy/dynamics/aircraft/complex_multirotor.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class v_smap_quat(ListEnum):
9999
100100
This state map replaces the Euler angles (roll, pitch, yaw) and DCM with
101101
a quaternion representation to avoid gimbal lock. The quaternion is stored
102-
in scalar-last format: [qx, qy, qz, qw] where qw is the scalar component.
102+
in scalar-first format: [qw, qx, qy, qz] where qw is the scalar component.
103103
"""
104104

105105
lat = (0, "rad")
@@ -110,7 +110,7 @@ class v_smap_quat(ListEnum):
110110
ned_pos = ([4, 5, 6], "m")
111111
ned_vel = ([7, 8, 9], "m/s")
112112
ned_accel = ([10, 11, 12], "m/s^2")
113-
quat = ([13, 14, 15, 16], "") # [qx, qy, qz, qw] - scalar last
113+
quat = ([13, 14, 15, 16], "") # [qw, qx, qy, qz] - scalar first
114114
body_vel = ([17, 18, 19], "m/s")
115115
body_accel = ([20, 21, 22], "m/s^2")
116116
body_rot_rate = ([23, 24, 25], "rad/s")
@@ -142,7 +142,7 @@ def _get_ordered_key(cls, key, append_ind):
142142
name = getattr(attr, key)
143143
if append_ind:
144144
if is_quat:
145-
quat_names = ["qx", "qy", "qz", "qw"]
145+
quat_names = ["qw", "qx", "qy", "qz"]
146146
name += "_" + quat_names[ii]
147147
elif multi:
148148
name += "_{:d}".format(ii)
@@ -315,7 +315,7 @@ def ode_quat(t, x, f, m):
315315
State vector x:
316316
[0:3] NED position (m)
317317
[3:6] Body velocity (m/s)
318-
[6:10] Quaternion [qx, qy, qz, qw]
318+
[6:10] Quaternion [qw, qx, qy, qz] (scalar first)
319319
[10:13] Body angular rates (rad/s)
320320
"""
321321
# Extract state components
@@ -340,14 +340,14 @@ def ode_quat(t, x, f, m):
340340
xdot[3:6] = f / self.params.mass.mass_kg + np.cross(omega, body_vel)
341341

342342
# Quaternion derivative (kinematics)
343-
# qdot = 0.5 * Omega(omega) * q, where Omega is the skew-symmetric matrix
344-
qx, qy, qz, qw = quat
343+
# qdot = 0.5 * Omega(omega) * q for scalar-first [qw, qx, qy, qz]
344+
qw, qx, qy, qz = quat
345345
wx, wy, wz = omega
346346

347-
xdot[6] = 0.5 * (-wx * qx - wy * qy - wz * qz) # qx_dot
348-
xdot[7] = 0.5 * (wx * qw + wz * qx - wy * qz) # qy_dot
349-
xdot[8] = 0.5 * (wy * qw - wz * qy + wx * qz) # qz_dot
350-
xdot[9] = 0.5 * (wz * qw + wy * qx - wx * qy) # qw_dot
347+
xdot[6] = 0.5 * (-wx * qx - wy * qy - wz * qz) # qw_dot
348+
xdot[7] = 0.5 * (wx * qw + wz * qy - wy * qz) # qx_dot
349+
xdot[8] = 0.5 * (wy * qw - wz * qx + wx * qz) # qy_dot
350+
xdot[9] = 0.5 * (wz * qw + wy * qx - wx * qy) # qz_dot
351351

352352
# Angular velocity derivative (Euler's equation)
353353
J = np.array(self.params.mass.inertia_kgm2)

src/gncpy/math/__init__.py

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ def quat_normalize(q):
462462
Parameters
463463
----------
464464
q : numpy array
465-
Quaternion [qx, qy, qz, qw] (scalar last, Hamilton convention)
465+
Quaternion [qw, qx, qy, qz] (scalar first)
466466
467467
Returns
468468
-------
@@ -471,36 +471,36 @@ def quat_normalize(q):
471471
"""
472472
mag = np.linalg.norm(q)
473473
if mag < np.finfo(float).eps:
474-
return np.array([0.0, 0.0, 0.0, 1.0])
474+
return np.array([1.0, 0.0, 0.0, 0.0])
475475
return q / mag
476476

477477

478478
def quat_multiply(q1, q2):
479479
"""Multiply two quaternions using Hamilton convention.
480480
481-
Computes q1 * q2 in scalar-last format [qx, qy, qz, qw].
481+
Computes q1 * q2 in scalar-first format [qw, qx, qy, qz].
482482
483483
Parameters
484484
----------
485485
q1 : numpy array
486-
First quaternion [qx, qy, qz, qw]
486+
First quaternion [qw, qx, qy, qz]
487487
q2 : numpy array
488-
Second quaternion [qx, qy, qz, qw]
488+
Second quaternion [qw, qx, qy, qz]
489489
490490
Returns
491491
-------
492492
numpy array
493493
Product quaternion q1 * q2
494494
"""
495-
qx1, qy1, qz1, qw1 = q1
496-
qx2, qy2, qz2, qw2 = q2
495+
qw1, qx1, qy1, qz1 = q1
496+
qw2, qx2, qy2, qz2 = q2
497497

498498
return np.array(
499499
[
500+
qw1 * qw2 - qx1 * qx2 - qy1 * qy2 - qz1 * qz2,
500501
qw1 * qx2 + qx1 * qw2 + qy1 * qz2 - qz1 * qy2,
501502
qw1 * qy2 - qx1 * qz2 + qy1 * qw2 + qz1 * qx2,
502503
qw1 * qz2 + qx1 * qy2 - qy1 * qx2 + qz1 * qw2,
503-
qw1 * qw2 - qx1 * qx2 - qy1 * qy2 - qz1 * qz2,
504504
]
505505
)
506506

@@ -511,14 +511,14 @@ def quat_conjugate(q):
511511
Parameters
512512
----------
513513
q : numpy array
514-
Quaternion [qx, qy, qz, qw]
514+
Quaternion [qw, qx, qy, qz]
515515
516516
Returns
517517
-------
518518
numpy array
519-
Conjugate quaternion [-qx, -qy, -qz, qw]
519+
Conjugate quaternion [qw, -qx, -qy, -qz]
520520
"""
521-
return np.array([-q[0], -q[1], -q[2], q[3]])
521+
return np.array([q[0], -q[1], -q[2], -q[3]])
522522

523523

524524
def quat_inverse(q):
@@ -529,7 +529,7 @@ def quat_inverse(q):
529529
Parameters
530530
----------
531531
q : numpy array
532-
Quaternion [qx, qy, qz, qw]
532+
Quaternion [qw, qx, qy, qz]
533533
534534
Returns
535535
-------
@@ -550,7 +550,7 @@ def quat_rotate_vector(q, v):
550550
Parameters
551551
----------
552552
q : numpy array
553-
Quaternion [qx, qy, qz, qw]
553+
Quaternion [qw, qx, qy, qz]
554554
v : numpy array
555555
3D vector to rotate
556556
@@ -559,37 +559,51 @@ def quat_rotate_vector(q, v):
559559
numpy array
560560
Rotated 3D vector
561561
"""
562-
# Convert vector to pure quaternion [vx, vy, vz, 0]
563-
v_quat = np.array([v[0], v[1], v[2], 0.0])
562+
# Convert vector to pure quaternion [0, vx, vy, vz]
563+
v_quat = np.array([0.0, v[0], v[1], v[2]])
564564

565565
# Perform rotation: q * v * q_conj
566566
q_conj = quat_conjugate(q)
567567
result = quat_multiply(quat_multiply(q, v_quat), q_conj)
568568

569-
return result[0:3]
569+
return result[1:4]
570570

571571

572572
def quat_to_dcm(q):
573573
"""Convert quaternion to direction cosine matrix (DCM).
574574
575+
Implements equation (6.79) from the textbook.
576+
575577
Parameters
576578
----------
577579
q : numpy array
578-
Quaternion [qx, qy, qz, qw] (scalar last, Hamilton convention)
580+
Quaternion [qw, qx, qy, qz]
579581
580582
Returns
581583
-------
582584
numpy array
583585
3x3 DCM for rotation from reference frame to body frame
584586
"""
585-
qx, qy, qz, qw = q
587+
qw, qx, qy, qz = q
586588

587-
# DCM from quaternion
589+
# DCM from quaternion (equation 6.79)
588590
dcm = np.array(
589591
[
590-
[1 - 2 * (qy**2 + qz**2), 2 * (qx * qy + qw * qz), 2 * (qx * qz - qw * qy)],
591-
[2 * (qx * qy - qw * qz), 1 - 2 * (qx**2 + qz**2), 2 * (qy * qz + qw * qx)],
592-
[2 * (qx * qz + qw * qy), 2 * (qy * qz - qw * qx), 1 - 2 * (qx**2 + qy**2)],
592+
[
593+
qw**2 + qx**2 - qy**2 - qz**2,
594+
2 * (qx * qy + qw * qz),
595+
2 * (qx * qz - qw * qy),
596+
],
597+
[
598+
2 * (qx * qy - qw * qz),
599+
qw**2 - qx**2 + qy**2 - qz**2,
600+
2 * (qy * qz + qw * qx),
601+
],
602+
[
603+
2 * (qx * qz + qw * qy),
604+
2 * (qy * qz - qw * qx),
605+
qw**2 - qx**2 - qy**2 + qz**2,
606+
],
593607
]
594608
)
595609
return dcm
@@ -608,7 +622,7 @@ def dcm_to_quat(dcm):
608622
Returns
609623
-------
610624
numpy array
611-
Quaternion [qx, qy, qz, qw] (scalar last)
625+
Quaternion [qw, qx, qy, qz]
612626
"""
613627
trace = np.trace(dcm)
614628

@@ -637,7 +651,7 @@ def dcm_to_quat(dcm):
637651
qy = (dcm[1, 2] + dcm[2, 1]) / s
638652
qz = 0.25 * s
639653

640-
return np.array([qx, qy, qz, qw])
654+
return np.array([qw, qx, qy, qz])
641655

642656

643657
def quat_to_euler(q):
@@ -646,7 +660,7 @@ def quat_to_euler(q):
646660
Parameters
647661
----------
648662
q : numpy array
649-
Quaternion [qx, qy, qz, qw] (scalar last)
663+
Quaternion [qw, qx, qy, qz]
650664
651665
Returns
652666
-------
@@ -657,7 +671,7 @@ def quat_to_euler(q):
657671
yaw : float
658672
Yaw angle in radians (rotation about z-axis)
659673
"""
660-
qx, qy, qz, qw = q
674+
qw, qx, qy, qz = q
661675

662676
# Roll (x-axis rotation)
663677
sinr_cosp = 2 * (qw * qx + qy * qz)
@@ -694,7 +708,7 @@ def euler_to_quat(roll, pitch, yaw):
694708
Returns
695709
-------
696710
numpy array
697-
Quaternion [qx, qy, qz, qw] (scalar last)
711+
Quaternion [qw, qx, qy, qz]
698712
"""
699713
cy = np.cos(yaw * 0.5)
700714
sy = np.sin(yaw * 0.5)
@@ -708,7 +722,7 @@ def euler_to_quat(roll, pitch, yaw):
708722
qy = cr * sp * cy + sr * cp * sy
709723
qz = cr * cp * sy - sr * sp * cy
710724

711-
return np.array([qx, qy, qz, qw])
725+
return np.array([qw, qx, qy, qz])
712726

713727

714728
def quat_slerp(q1, q2, t):
@@ -717,9 +731,9 @@ def quat_slerp(q1, q2, t):
717731
Parameters
718732
----------
719733
q1 : numpy array
720-
Start quaternion [qx, qy, qz, qw]
734+
Start quaternion [qw, qx, qy, qz]
721735
q2 : numpy array
722-
End quaternion [qx, qy, qz, qw]
736+
End quaternion [qw, qx, qy, qz]
723737
t : float
724738
Interpolation parameter in [0, 1]
725739

0 commit comments

Comments
 (0)