├── .gitignore ├── README.md ├── adcs.png ├── attitude ├── Makefile ├── README.md ├── attitude.h ├── axan.c ├── axan.h ├── dcm.c ├── dcm.h ├── euler.c ├── euler.h ├── quat.c ├── quat.h └── test_att.c ├── igrf ├── README.md ├── c │ ├── Makefile │ ├── igrf.c │ ├── igrf.h │ ├── igrf_2025.h │ └── main.c └── matlab │ ├── igrf.m │ ├── igrf_coeffs │ ├── igrf_2020.txt │ └── norm_g_2020.txt │ └── main.m └── spacetime ├── Makefile ├── README.md ├── frame.c ├── frame.h ├── iau06 ├── iau06.c ├── iau06.h ├── iau06_data.c └── iau06_data.h ├── test_frame.c ├── tests.c ├── time.c └── time.h /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .vscode 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adcs 2 | 3 |

4 | 5 |

6 | 7 | Utilities and algorithms for the Attitude Determination and Control System (ADCS) of spacecraft, targeted toward embedded hardware. 8 | 9 | ## Contents 10 | 11 | 1. [igrf](/igrf/) - International Geomagnetic Reference Field (IGRF-14) Model 12 | 2. [attitude](https://github.com/risherlock/adcs/tree/attitude/attitude) - Basic tools for rotational manipulations using SO(3), quaternions, and Euler angles 13 | 3. [spacetime](https://github.com/risherlock/adcs/tree/attitude/spacetime) - Coordinate transformations and time conversion utilities. 14 | -------------------------------------------------------------------------------- /adcs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/risherlock/adcs/a3254bbc516fe128b21881ecd44f862df9ba0701/adcs.png -------------------------------------------------------------------------------- /attitude/Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | CFLAGS = -std=c99 -Wall -Wextra -Werror -lm -O3 3 | 4 | TARGET = test 5 | BUILD_DIR = build 6 | 7 | INC = 8 | SRC = dcm.c euler.c quat.c axan.c test_att.c 9 | 10 | OBJ = $(SRC:%.c=$(BUILD_DIR)/%.o) 11 | 12 | $(BUILD_DIR)/%.o: %.c | $(BUILD_DIR) 13 | $(CC) $(CFLAGS) $(INC) -c $< -o $@ 14 | 15 | $(TARGET): $(OBJ) 16 | $(CC) $(CFLAGS) $(INC) -o $(BUILD_DIR)/$(TARGET) $(OBJ) 17 | -------------------------------------------------------------------------------- /attitude/README.md: -------------------------------------------------------------------------------- 1 | # attitude 2 | 3 | Basic rotation manipulations for ADCS implemented using the C99 standard. 4 | 5 | ## Notes 6 | 7 | 1. DCM represents rotation from intertial to body frame. 8 | 9 | ### Todos 10 | 11 | 1. Unit tests. 12 | 13 | ### Queries 14 | 15 | 1. Normalize error quaternion? 16 | 2. Normalize quaternion from axis-angle? 17 | 3. Euler sequences enum versus hard-code. 18 | 19 | ## References 20 | 21 | 1. Henderson - Euler Angles, quaternions, and transformation matrices: Working relationships (1977) 22 | 2. Bernardes, Viollet - Quaternion to Euler angles conversion: A direct, general and computationally efficient method (2022) 23 | 3. Motekew - Quaternion to DCM and back again (2014) 24 | 4. Waveren - From quaternion to matrix and back (2005) 25 | 5. Schaub, Junkins - Analytical mechanics of space systems - 4th Ed. (2018) 26 | 6. Shoemake - Animating rotation with Quaternion Curves (1985) 27 | 7. Markley et.al. - Averaging quaternions (2007) 28 | -------------------------------------------------------------------------------- /attitude/attitude.h: -------------------------------------------------------------------------------- 1 | #ifndef _ATTITUDE_H_ 2 | #define _ATTITUDE_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include "dcm.h" 9 | #include "quat.h" 10 | #include "euler.h" 11 | 12 | #endif // attitude.h 13 | -------------------------------------------------------------------------------- /attitude/axan.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "axan.h" 3 | 4 | /** 5 | * @brief Computes the quaternion corresponding to a rotation by an angle `psi` [rad] about the unit 6 | * axis vector `v`. 7 | * 8 | * @param psi Rotation angle in radians 9 | * @param v Unit vector representing the axis of rotation 10 | * @param q Output quaternion 11 | */ 12 | void axan_to_quat(const double psi, const double v[3], double q[4]) 13 | { 14 | double hp = psi / 2.0; 15 | double shp = sin(hp); 16 | 17 | q[0] = cos(hp); 18 | q[1] = shp * v[0]; 19 | q[2] = shp * v[1]; 20 | q[3] = shp * v[2]; 21 | } 22 | -------------------------------------------------------------------------------- /attitude/axan.h: -------------------------------------------------------------------------------- 1 | // Basic axis-angle transformations. 2 | // rms (2025-04-02 1:19:22 AM) 3 | 4 | #ifndef _ATTITUDE_AXAN_H_ 5 | #define _ATTITUDE_AXAN_H_ 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | void axan_to_quat(const double psi, const double v[3], double q[4]); 12 | 13 | #ifdef __cplusplus 14 | } 15 | #endif 16 | 17 | #endif // axan.h 18 | -------------------------------------------------------------------------------- /attitude/dcm.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "dcm.h" 5 | 6 | // DCM corresponding to a rotation by angle `xi` [rad] about the x-axis. 7 | void dcm_x(const double xi, double m[3][3]) 8 | { 9 | m[0][0] = 1.0; m[1][0] = 0.0; m[2][0] = 0.0; 10 | m[0][1] = 0.0; m[1][1] = cos(xi); m[2][1] = -sin(xi); 11 | m[0][2] = 0.0; m[1][2] = sin(xi); m[2][2] = cos(xi); 12 | } 13 | 14 | // DCM corresponding to a rotation by angle `xi` [rad] about the y-axis. 15 | void dcm_y(const double xi, double m[3][3]) 16 | { 17 | m[0][0] = cos(xi); m[1][0] = 0.0; m[2][0] = sin(xi); 18 | m[0][1] = 0.0; m[1][1] = 1.0; m[2][1] = 0.0; 19 | m[0][2] = -sin(xi); m[1][2] = 0.0; m[2][2] = cos(xi); 20 | } 21 | 22 | // DCM corresponding to a rotation by angle `xi` [rad] about the z-axis. 23 | void dcm_z(const double xi, double m[3][3]) 24 | { 25 | m[0][0] = cos(xi); m[1][0] = -sin(xi); m[2][0] = 0.0; 26 | m[0][1] = sin(xi); m[1][1] = cos(xi); m[2][1] = 0.0; 27 | m[0][2] = 0.0; m[1][2] = 0.0; m[2][2] = 1.0; 28 | } 29 | 30 | // DCM corresponding to no rotation. 31 | void dcm_unit(double m[3][3]) 32 | { 33 | m[0][0] = 1.0; m[0][1] = 0.0; m[0][2] = 0.0; 34 | m[1][0] = 0.0; m[1][1] = 1.0; m[1][2] = 0.0; 35 | m[2][0] = 0.0; m[2][1] = 0.0; m[2][2] = 1.0; 36 | } 37 | 38 | // Computes transpose of input DCM/ 39 | void dcm_trans(const double m[3][3], double t[3][3]) 40 | { 41 | for (int i = 0; i < 3; i++) 42 | { 43 | for (int j = 0; j < 3; j++) { 44 | t[j][i] = m[i][j]; 45 | } 46 | } 47 | } 48 | 49 | // DCM multiplication: m = a * b 50 | void dcm_prod(const double a[3][3], const double b[3][3], double m[3][3]) 51 | { 52 | for (int i = 0; i < 3; i++) 53 | { 54 | for (int j = 0; j < 3; j++) 55 | { 56 | m[i][j] = 0.0; 57 | 58 | for (int k = 0; k < 3; k++) 59 | { 60 | m[i][j] += a[i][k] * b[k][j]; 61 | } 62 | } 63 | } 64 | } 65 | 66 | // Rotates the input vector `v` using the DCM `m` to produce the output vector `v_out`. 67 | void dcm_rotate(const double m[3][3], const double v[3], double v_out[3]) 68 | { 69 | for (int i = 0; i < 3; i++) 70 | { 71 | v_out[i] = 0.0; 72 | 73 | for (int j = 0; j < 3; j++) 74 | { 75 | v_out[i] += m[i][j] * v[j]; 76 | } 77 | } 78 | } 79 | 80 | // Computes the quaternion corresponding to the input DCM. 81 | void dcm_to_quat(const double r[3][3], double q[4]) 82 | { 83 | double trace = r[0][0] + r[1][1] + r[2][2]; 84 | double s; 85 | 86 | if (trace > 0) 87 | { 88 | s = 2.0 * sqrt(1.0 + trace); 89 | q[0] = 0.25 * s; 90 | q[1] = (r[2][1] - r[1][2]) / s; 91 | q[2] = (r[0][2] - r[2][0]) / s; 92 | q[3] = (r[1][0] - r[0][1]) / s; 93 | } 94 | else 95 | { 96 | if (r[0][0] > r[1][1] && r[0][0] > r[2][2]) 97 | { 98 | s = 2.0 * sqrt(1.0 + r[0][0] - r[1][1] - r[2][2]); 99 | q[0] = (r[2][1] - r[1][2]) / s; 100 | q[1] = 0.25 * s; 101 | q[2] = (r[0][1] + r[1][0]) / s; 102 | q[3] = (r[0][2] + r[2][0]) / s; 103 | } 104 | else if (r[1][1] > r[2][2]) 105 | { 106 | s = 2.0 * sqrt(1.0 + r[1][1] - r[0][0] - r[2][2]); 107 | q[0] = (r[0][2] - r[2][0]) / s; 108 | q[1] = (r[0][1] + r[1][0]) / s; 109 | q[2] = 0.25 * s; 110 | q[3] = (r[1][2] + r[2][1]) / s; 111 | } 112 | else 113 | { 114 | s = 2.0 * sqrt(1.0 + r[2][2] - r[0][0] - r[1][1]) * 2; 115 | q[0] = (r[1][0] - r[0][1]) / s; 116 | q[1] = (r[0][2] + r[2][0]) / s; 117 | q[2] = (r[1][2] + r[2][1]) / s; 118 | q[3] = 0.25 * s; 119 | } 120 | } 121 | } 122 | 123 | // Computes Euler angles from the input DCM using the sequence specified by `es`. 124 | void dcm_to_euler(const double m[3][3], double e[3], const euler_seq_t es) 125 | { 126 | switch (es) 127 | { 128 | case EULER_XYZ: 129 | { 130 | e[0] = atan2(-m[2][1], m[2][2]); 131 | e[1] = asin(m[2][0]); 132 | e[2] = atan2(-m[1][0], m[0][0]); 133 | break; 134 | } 135 | 136 | case EULER_XZY: 137 | { 138 | e[0] = atan2(m[1][2], m[1][1]); 139 | e[1] = asin(-m[1][0]); 140 | e[2] = atan2(m[2][0], m[0][0]); 141 | break; 142 | } 143 | 144 | case EULER_XYX: 145 | { 146 | e[0] = atan2(m[0][1], -m[0][2]); 147 | e[1] = acos(m[0][0]); 148 | e[2] = atan2(m[1][0], m[2][0]); 149 | break; 150 | } 151 | 152 | case EULER_XZX: 153 | { 154 | e[0] = atan2(m[0][2], m[0][1]); 155 | e[1] = acos(m[0][0]); 156 | e[2] = atan2(m[2][0], -m[1][0]); 157 | break; 158 | } 159 | 160 | case EULER_YXZ: 161 | { 162 | e[0] = atan2(m[2][0], m[2][2]); 163 | e[1] = asin(-m[2][1]); 164 | e[2] = atan2(m[0][1], m[1][1]); 165 | break; 166 | } 167 | 168 | case EULER_YZX: 169 | { 170 | e[0] = atan2(-m[0][2], m[0][0]); 171 | e[1] = asin(m[0][1]); 172 | e[2] = atan2(-m[2][1], m[1][1]); 173 | break; 174 | } 175 | 176 | case EULER_YXY: 177 | { 178 | e[0] = atan2(m[1][0], m[1][2]); 179 | e[1] = acos(m[1][1]); 180 | e[2] = atan2(m[0][1], -m[2][1]); 181 | break; 182 | } 183 | 184 | case EULER_YZY: 185 | { 186 | e[0] = atan2(m[1][2], -m[1][0]); 187 | e[1] = acos(m[1][1]); 188 | e[2] = atan2(m[2][1], m[0][1]); 189 | break; 190 | } 191 | 192 | case EULER_ZXY: 193 | { 194 | e[0] = atan2(-m[1][0], m[1][1]); 195 | e[1] = asin(m[1][2]); 196 | e[2] = atan2(-m[0][2], m[2][2]); 197 | break; 198 | } 199 | 200 | case EULER_ZYX: 201 | { 202 | e[0] = atan2(m[0][1], m[0][0]); 203 | e[1] = asin(-m[0][2]); 204 | e[2] = atan2(m[1][2], m[2][2]); 205 | break; 206 | } 207 | 208 | case EULER_ZXZ: 209 | { 210 | e[0] = atan2(m[2][0], -m[2][1]); 211 | e[1] = acos(m[2][2]); 212 | e[2] = atan2(m[0][2], m[1][2]); 213 | break; 214 | } 215 | 216 | case EULER_ZYZ: 217 | { 218 | e[0] = atan2(m[2][1], m[2][0]); 219 | e[1] = acos(m[2][2]); 220 | e[2] = atan2(m[1][2], -m[0][2]); 221 | break; 222 | } 223 | } 224 | } 225 | 226 | /** 227 | * @brief Time derivative of DCM for given angular velocity expressed in the body frame. 228 | * 229 | * @note This function assumes that the input DCM represents a rotation from the inertial frame to 230 | * the body frame. 231 | * 232 | * @warning If the DCM represents a rotation from the body frame to the inertial frame, the 233 | * following equation applies: m_dot = m x [w]. In this case, the angular velocity is 234 | * expressed in the inertial frame. 235 | * 236 | * @todo Explore how same function would be used for the case in the warning. 237 | */ 238 | void dcm_rate(const double w[3], const double m[3][3], double m_dot[3][3]) 239 | { 240 | // m_dot = -[w] x m where, [w] is skew-symmetric matrix of w. 241 | const double wx[3][3] = {{0.0, w[2], -w[1]}, {-w[2], 0.0, w[0]}, {w[1], -w[0], 0.0}}; 242 | dcm_prod(wx, m, m_dot); 243 | } 244 | -------------------------------------------------------------------------------- /attitude/dcm.h: -------------------------------------------------------------------------------- 1 | // Basic Direction Cosine Matrix manipulations. 2 | // 2024-12-23 3 | 4 | #ifndef _ATTITUDE_DCM_H_ 5 | #define _ATTITUDE_DCM_H_ 6 | 7 | #include "euler.h" 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | void dcm_unit(double m[3][3]); 14 | void dcm_x(const double xi, double m[3][3]); 15 | void dcm_y(const double xi, double m[3][3]); 16 | void dcm_z(const double xi, double m[3][3]); 17 | void dcm_trans(const double m[3][3], double t[3][3]); 18 | void dcm_prod(const double a[3][3], const double b[3][3], double m[3][3]); 19 | void dcm_rotate(const double m[3][3], const double v[3], double v_out[3]); 20 | void dcm_to_euler(const double m[3][3], double e[3], const euler_seq_t es); 21 | void dcm_rate(const double w[3], const double m[3][3], double m_dot[3][3]); 22 | 23 | #ifdef __cplusplus 24 | } 25 | #endif 26 | 27 | #endif // dcm.h 28 | -------------------------------------------------------------------------------- /attitude/euler.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "euler.h" 6 | 7 | // Computes quaternion from the input Euler angles using the sequence specified by `es`. 8 | void euler_to_quat(const double e[3], const euler_seq_t es, double q[4]) 9 | { 10 | const double c[3] = {cos(0.5 * e[0]), cos(0.5 * e[1]), cos(0.5 * e[2])}; 11 | const double s[3] = {sin(0.5 * e[0]), sin(0.5 * e[1]), sin(0.5 * e[2])}; 12 | const double h[3] = {0.5 * e[0], 0.5 * e[1], 0.5 * e[2]}; 13 | const double ch = cos(h[1]); 14 | const double sh = sin(h[1]); 15 | 16 | switch (es) 17 | { 18 | case EULER_XYZ: 19 | { 20 | q[0] = c[0] * c[1] * c[2] - s[0] * s[1] * s[2]; 21 | q[1] = s[0] * c[1] * c[2] + c[0] * s[1] * s[2]; 22 | q[2] = c[0] * s[1] * c[2] - s[0] * c[1] * s[2]; 23 | q[3] = c[0] * c[1] * s[2] + s[0] * s[1] * c[2]; 24 | break; 25 | } 26 | 27 | case EULER_XZY: 28 | { 29 | q[0] = c[0] * c[1] * c[2] + s[0] * s[1] * s[2]; 30 | q[1] = s[0] * c[1] * c[2] - c[0] * s[1] * s[2]; 31 | q[2] = c[0] * c[1] * s[2] - s[0] * s[1] * c[2]; 32 | q[3] = c[0] * s[1] * c[2] + s[0] * c[1] * s[2]; 33 | break; 34 | } 35 | 36 | case EULER_XYX: 37 | { 38 | q[0] = ch * cos(h[0] + h[2]); 39 | q[1] = ch * sin(h[0] + h[2]); 40 | q[2] = sh * cos(h[0] - h[2]); 41 | q[3] = sh * sin(h[0] - h[2]); 42 | break; 43 | } 44 | 45 | case EULER_XZX: 46 | { 47 | q[0] = ch * cos(h[0] + h[2]); 48 | q[1] = ch * sin(h[0] + h[2]); 49 | q[2] = sh * sin(-h[0] + h[2]); 50 | q[3] = sh * cos(-h[0] + h[2]); 51 | break; 52 | } 53 | 54 | case EULER_YXZ: 55 | { 56 | q[0] = c[0] * c[1] * c[2] + s[0] * s[1] * s[2]; 57 | q[1] = c[0] * s[1] * c[2] + s[0] * c[1] * s[2]; 58 | q[2] = s[0] * c[1] * c[2] - c[0] * s[1] * s[2]; 59 | q[3] = c[0] * c[1] * s[2] - s[0] * s[1] * c[2]; 60 | break; 61 | } 62 | 63 | case EULER_YZX: 64 | { 65 | q[0] = c[0] * c[1] * c[2] - s[0] * s[1] * s[2]; 66 | q[1] = c[0] * c[1] * s[2] + s[0] * s[1] * c[2]; 67 | q[2] = s[0] * c[1] * c[2] + c[0] * s[1] * s[2]; 68 | q[3] = c[0] * s[1] * c[2] - s[0] * c[1] * s[2]; 69 | break; 70 | } 71 | 72 | case EULER_YXY: 73 | { 74 | q[0] = ch * cos(h[0] + h[2]); 75 | q[1] = sh * cos(-h[0] + h[2]); 76 | q[2] = ch * sin(h[0] + h[2]); 77 | q[3] = sh * sin(-h[0] + h[2]); 78 | break; 79 | } 80 | 81 | case EULER_YZY: 82 | { 83 | q[0] = ch * cos(h[0] + h[2]); 84 | q[1] = sh * sin(h[0] - h[2]); 85 | q[2] = ch * sin(h[0] + h[2]); 86 | q[3] = sh * cos(h[0] - h[2]); 87 | break; 88 | } 89 | 90 | case EULER_ZXY: 91 | { 92 | q[0] = c[0] * c[1] * c[2] - s[0] * s[1] * s[2]; 93 | q[1] = c[0] * s[1] * c[2] - s[0] * c[1] * s[2]; 94 | q[2] = c[0] * c[1] * s[2] + s[0] * s[1] * c[2]; 95 | q[3] = s[0] * c[1] * c[2] + c[0] * s[1] * s[2]; 96 | break; 97 | } 98 | 99 | case EULER_ZYX: 100 | { 101 | q[0] = c[0] * c[1] * c[2] + s[0] * s[1] * s[2]; 102 | q[1] = c[0] * c[1] * s[2] - s[0] * s[1] * c[2]; 103 | q[2] = c[0] * s[1] * c[2] + s[0] * c[1] * s[2]; 104 | q[3] = s[0] * c[1] * c[2] - c[0] * s[1] * s[2]; 105 | 106 | break; 107 | } 108 | 109 | case EULER_ZXZ: 110 | { 111 | q[0] = ch * cos(h[0] + h[2]); 112 | q[1] = sh * cos(h[0] - h[2]); 113 | q[2] = sh * sin(h[0] - h[2]); 114 | q[3] = ch * sin(h[0] + h[2]); 115 | break; 116 | } 117 | 118 | case EULER_ZYZ: 119 | { 120 | q[0] = ch * cos(h[0] + h[2]); 121 | q[1] = sh * sin(-h[0] + h[2]); 122 | q[2] = sh * cos(-h[0] + h[2]); 123 | q[3] = ch * sin(h[0] + h[2]); 124 | break; 125 | } 126 | } 127 | } 128 | 129 | // Computes DCM from the input Euler angles using the sequence specified by `es`. 130 | void euler_to_dcm(const double e[3], const euler_seq_t es, double m[3][3]) 131 | { 132 | const double c[3] = {cos(e[0]), cos(e[1]), cos(e[2])}; 133 | const double s[3] = {sin(e[0]), sin(e[1]), sin(e[2])}; 134 | 135 | switch (es) 136 | { 137 | case EULER_XYZ: 138 | { 139 | m[0][0] = c[1] * c[2]; 140 | m[0][1] = c[2] * s[0] * s[1] + c[0] * s[2]; 141 | m[0][2] = s[0] * s[2] - c[0] * c[2] * s[1]; 142 | m[1][0] = -c[1] * s[2]; 143 | m[1][1] = c[0] * c[2] - s[0] * s[1] * s[2]; 144 | m[1][2] = c[2] * s[0] + c[0] * s[1] * s[2]; 145 | m[2][0] = s[1]; 146 | m[2][1] = -c[1] * s[0]; 147 | m[2][2] = c[0] * c[1]; 148 | break; 149 | } 150 | 151 | case EULER_XZY: 152 | { 153 | m[0][0] = c[1] * c[2]; 154 | m[0][1] = c[0] * c[2] * s[1] + s[0] * s[2]; 155 | m[0][2] = c[2] * s[0] * s[1] - c[0] * s[2]; 156 | m[1][0] = -s[1]; 157 | m[1][1] = c[0] * c[1]; 158 | m[1][2] = c[1] * s[0]; 159 | m[2][0] = c[1] * s[2]; 160 | m[2][1] = -c[2] * s[0] + c[0] * s[1] * s[2]; 161 | m[2][2] = c[0] * c[2] + s[0] * s[1] * s[2]; 162 | break; 163 | } 164 | 165 | case EULER_XYX: 166 | { 167 | m[0][0] = c[1]; 168 | m[0][1] = s[0] * s[1]; 169 | m[0][2] = -c[0] * s[1]; 170 | m[1][0] = s[1] * s[2]; 171 | m[1][1] = c[0] * c[2] - c[1] * s[0] * s[2]; 172 | m[1][2] = c[2] * s[0] + c[0] * c[1] * s[2]; 173 | m[2][0] = c[2] * s[1]; 174 | m[2][1] = -c[1] * c[2] * s[0] - c[0] * s[2]; 175 | m[2][2] = c[0] * c[1] * c[2] - s[0] * s[2]; 176 | break; 177 | } 178 | 179 | case EULER_XZX: 180 | { 181 | m[0][0] = c[1]; 182 | m[0][1] = c[0] * s[1]; 183 | m[0][2] = s[0] * s[1]; 184 | m[1][0] = -c[2] * s[1]; 185 | m[1][1] = c[0] * c[1] * c[2] - s[0] * s[2]; 186 | m[1][2] = c[1] * c[2] * s[0] + c[0] * s[2]; 187 | m[2][0] = s[1] * s[2]; 188 | m[2][1] = -c[2] * s[0] - c[0] * c[1] * s[2]; 189 | m[2][2] = c[0] * c[2] - c[1] * s[0] * s[2]; 190 | break; 191 | } 192 | 193 | case EULER_YXZ: 194 | { 195 | m[0][0] = c[0] * c[2] + s[0] * s[1] * s[2]; 196 | m[0][1] = c[1] * s[2]; 197 | m[0][2] = -c[2] * s[0] + c[0] * s[1] * s[2]; 198 | m[1][0] = c[2] * s[0] * s[1] - c[0] * s[2]; 199 | m[1][1] = c[1] * c[2]; 200 | m[1][2] = c[0] * c[2] * s[1] + s[0] * s[2]; 201 | m[2][0] = c[1] * s[0]; 202 | m[2][1] = -s[1]; 203 | m[2][2] = c[0] * c[1]; 204 | break; 205 | } 206 | 207 | case EULER_YZX: 208 | { 209 | m[0][0] = c[0] * c[1]; 210 | m[0][1] = s[1]; 211 | m[0][2] = -c[1] * s[0]; 212 | m[1][0] = -c[0] * c[2] * s[1] + s[0] * s[2]; 213 | m[1][1] = c[1] * c[2]; 214 | m[1][2] = c[2] * s[0] * s[1] + c[0] * s[2]; 215 | m[2][0] = c[2] * s[0] + c[0] * s[1] * s[2]; 216 | m[2][1] = -c[1] * s[2]; 217 | m[2][2] = c[0] * c[2] - s[0] * s[1] * s[2]; 218 | break; 219 | } 220 | 221 | case EULER_YXY: 222 | { 223 | m[0][0] = c[0] * c[2] - c[1] * s[0] * s[2]; 224 | m[0][1] = s[1] * s[2]; 225 | m[0][2] = -c[2] * s[0] - c[0] * c[1] * s[2]; 226 | m[1][0] = s[0] * s[1]; 227 | m[1][1] = c[1]; 228 | m[1][2] = c[0] * s[1]; 229 | m[2][0] = c[1] * c[2] * s[0] + c[0] * s[2]; 230 | m[2][1] = -c[2] * s[1]; 231 | m[2][2] = c[0] * c[1] * c[2] - s[0] * s[2]; 232 | break; 233 | } 234 | 235 | case EULER_YZY: 236 | { 237 | m[0][0] = c[0] * c[1] * c[2] - s[0] * s[2]; 238 | m[0][1] = c[2] * s[1]; 239 | m[0][2] = -c[1] * c[2] * s[0] - c[0] * s[2]; 240 | m[1][0] = -c[0] * s[1]; 241 | m[1][1] = c[1]; 242 | m[1][2] = s[0] * s[1]; 243 | m[2][0] = c[2] * s[0] + c[0] * c[1] * s[2]; 244 | m[2][1] = s[1] * s[2]; 245 | m[2][2] = c[0] * c[2] - c[1] * s[0] * s[2]; 246 | break; 247 | } 248 | 249 | case EULER_ZXY: 250 | { 251 | m[0][0] = c[0] * c[2] - s[0] * s[1] * s[2]; 252 | m[0][1] = c[2] * s[0] + c[0] * s[1] * s[2]; 253 | m[0][2] = -c[1] * s[2]; 254 | m[1][0] = -c[1] * s[0]; 255 | m[1][1] = c[0] * c[1]; 256 | m[1][2] = s[1]; 257 | m[2][0] = c[2] * s[0] * s[1] + c[0] * s[2]; 258 | m[2][1] = s[0] * s[2] - c[0] * c[2] * s[1]; 259 | m[2][2] = c[1] * c[2]; 260 | break; 261 | } 262 | 263 | case EULER_ZYX: 264 | { 265 | m[0][0] = c[1] * c[0]; 266 | m[0][1] = c[1] * s[0]; 267 | m[0][2] = -s[1]; 268 | m[1][0] = s[2] * s[1] * c[0] - c[2] * s[0]; 269 | m[1][1] = s[2] * s[1] * s[0] + c[2] * c[0]; 270 | m[1][2] = s[2] * c[1]; 271 | m[2][0] = c[2] * s[1] * c[0] + s[2] * s[0]; 272 | m[2][1] = c[2] * s[1] * s[0] - s[2] * c[0]; 273 | m[2][2] = c[2] * c[1]; 274 | break; 275 | } 276 | 277 | case EULER_ZXZ: 278 | { 279 | m[0][0] = c[2] * c[0] - s[2] * c[1] * s[0]; 280 | m[0][1] = c[2] * s[0] + s[2] * c[1] * c[0]; 281 | m[0][2] = s[2] * s[1]; 282 | m[1][0] = -s[2] * c[0] - c[2] * c[1] * s[0]; 283 | m[1][1] = -s[2] * s[0] + c[2] * c[1] * c[0]; 284 | m[1][2] = c[2] * s[1]; 285 | m[2][0] = s[1] * s[0]; 286 | m[2][1] = -s[1] * c[0]; 287 | m[2][2] = c[1]; 288 | break; 289 | } 290 | 291 | case EULER_ZYZ: 292 | { 293 | m[0][0] = c[0] * c[1] * c[2] - s[0] * s[2]; 294 | m[0][1] = c[1] * c[2] * s[0] + c[0] * s[2]; 295 | m[0][2] = -c[2] * s[1]; 296 | m[1][0] = -c[2] * s[0] - c[0] * c[1] * s[2]; 297 | m[1][1] = c[0] * c[2] - c[1] * s[0] * s[2]; 298 | m[1][2] = s[1] * s[2]; 299 | m[2][0] = c[0] * s[1]; 300 | m[2][1] = s[0] * s[1]; 301 | m[2][2] = c[1]; 302 | break; 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /attitude/euler.h: -------------------------------------------------------------------------------- 1 | // Basic Euler angles manipulations. 2 | // 2024-12-22 3 | 4 | #ifndef _ATTITUDE_EULER_H_ 5 | #define _ATTITUDE_EULER_H_ 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | typedef enum 12 | { 13 | EULER_XYX = 121, 14 | EULER_XYZ = 123, 15 | EULER_XZX = 131, 16 | EULER_XZY = 132, 17 | EULER_YXY = 212, 18 | EULER_YXZ = 213, 19 | EULER_YZX = 231, 20 | EULER_YZY = 232, 21 | EULER_ZXY = 312, 22 | EULER_ZXZ = 313, 23 | EULER_ZYX = 321, 24 | EULER_ZYZ = 323 25 | } euler_seq_t; 26 | 27 | void euler_to_quat(const double e[3], const euler_seq_t es, double q[4]); 28 | void euler_to_dcm(const double e[3], const euler_seq_t es, double m[3][3]); 29 | 30 | #ifdef __cplusplus 31 | } 32 | #endif 33 | 34 | #endif // euler.h 35 | -------------------------------------------------------------------------------- /attitude/quat.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "quat.h" 5 | 6 | #define PI_2 1.57079632679489661923 7 | #define PI 2 * 1.57079632679489661923 8 | 9 | #define QUAT_ANGLE_ON_SINGULARITY_RAD 0.0f 10 | #define QUAT_COMPARISON_EPSILON 1e-5f 11 | #define QUAT_DIVISION_EPSILON 1e-6f 12 | #define EULER_SINGULARITY_EPSILON 1e-7f 13 | 14 | // Initializes input array to the unit quaternion. 15 | void quat_unit(double q[4]) 16 | { 17 | q[0] = 1.0; 18 | q[1] = 0.0; 19 | q[2] = 0.0; 20 | q[3] = 0.0; 21 | } 22 | 23 | // Enforce q[0] >= 0 i.e. the canonical form. 24 | void quat_canonize(double q[4]) 25 | { 26 | if (q[0] < 0) 27 | { 28 | quat_neg(q); 29 | } 30 | } 31 | 32 | // Negates all the elements of the input quaternion. 33 | void quat_neg(double q[4]) 34 | { 35 | q[0] = -q[0]; 36 | q[1] = -q[1]; 37 | q[2] = -q[2]; 38 | q[3] = -q[3]; 39 | } 40 | 41 | // Normalizes the input quaternion. 42 | void quat_normalize(double q[4]) 43 | { 44 | double norm = quat_norm(q); 45 | 46 | if (norm > QUAT_DIVISION_EPSILON) 47 | { 48 | double inv_norm = 1.0 / norm; 49 | q[0] = q[0] * inv_norm; 50 | q[1] = q[1] * inv_norm; 51 | q[2] = q[2] * inv_norm; 52 | q[3] = q[3] * inv_norm; 53 | } 54 | } 55 | 56 | // Computes norm of the input quaternion. 57 | double quat_norm(const double q[4]) 58 | { 59 | return sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); 60 | } 61 | 62 | // Computes conjugate of the input quaternion. 63 | void quat_conj(const double q[4], double q_conj[4]) 64 | { 65 | q_conj[0] = q[0]; 66 | q_conj[1] = -q[1]; 67 | q_conj[2] = -q[2]; 68 | q_conj[3] = -q[3]; 69 | } 70 | 71 | // Copies the contents of `q_src` to `q_dest`. 72 | void quat_copy(const double q_src[4], double q_dest[4]) 73 | { 74 | q_dest[0] = q_src[0]; 75 | q_dest[1] = q_src[1]; 76 | q_dest[2] = q_src[2]; 77 | q_dest[3] = q_src[3]; 78 | } 79 | 80 | // Returns true if quaternions represent the same rotation (q1 == +/-q2). 81 | bool quat_equal(const double q1[4], const double q2[4]) 82 | { 83 | const double dot = quat_dot(q1, q2); 84 | return fabs(1.0 - fabs(dot)) <= QUAT_COMPARISON_EPSILON; 85 | } 86 | 87 | // Computes the dot product of the input quaternions. 88 | double quat_dot(const double q1[4], const double q2[4]) 89 | { 90 | return q1[0] * q2[0] + q1[1] * q2[1] + q1[2] * q2[2] + q1[3] * q2[3]; 91 | } 92 | 93 | // Angular distance [rad] between two quaternions. 94 | double quat_dist(const double q1[4], const double q2[4]) 95 | { 96 | double qe[4]; 97 | quat_err(q1, q2, qe); // Rotation from q2 to q1 98 | quat_normalize(qe); // Confirm domain for acos 99 | quat_canonize(qe); // Ensure the shortest path 100 | 101 | return 2.0 * acos(fabs(qe[0])); 102 | } 103 | 104 | // Quaternion product: q = q1 * q2 105 | void quat_prod(const double q1[4], const double q2[4], double q[4]) 106 | { 107 | q[0] = q1[0] * q2[0] - q1[1] * q2[1] - q1[2] * q2[2] - q1[3] * q2[3]; 108 | q[1] = q1[0] * q2[1] + q1[1] * q2[0] + q1[2] * q2[3] - q1[3] * q2[2]; 109 | q[2] = q1[0] * q2[2] - q1[1] * q2[3] + q1[2] * q2[0] + q1[3] * q2[1]; 110 | q[3] = q1[0] * q2[3] + q1[1] * q2[2] - q1[2] * q2[1] + q1[3] * q2[0]; 111 | } 112 | 113 | /** 114 | * @brief Computes the error quaternion representing the rotation from q2 to q1. 115 | * 116 | * @note In the setting of real numbers, x = x1 - x2 represents the offset that converts x2 to x1. 117 | * Analogously the quaternion q (q1 - q2) represents the rotation which rotates q2 to q1, 118 | * i.e. q1 = q * q2 => q = q1 * inv(q2). 119 | */ 120 | void quat_err(const double q1[4], const double q2[4], double q[4]) 121 | { 122 | double q2_inv[4]; 123 | quat_conj(q2, q2_inv); 124 | quat_prod(q1, q2_inv, q); 125 | } 126 | 127 | /** 128 | * @brief Computes the angle-axis representation corresponding to the input quaternion, where the 129 | * rotation occurs by an angle `psi` [rad] about the unit axis vector `v`. 130 | * 131 | * @param q Input quaternion 132 | * @param psi Rotation angle in radians 133 | * @param v Unit vector representing the axis of rotation 134 | * 135 | * @return Status of the conversion; `true` if successful, `false` if there are infinite solutions, 136 | * in which case the axis is chosen to be [1, 0, 0]'. 137 | */ 138 | bool quat_to_axan(const double q[4], double *psi, double v[3]) 139 | { 140 | bool status = true; 141 | *psi = 2.0 * acos(q[0]); 142 | double divisor = sqrt(1.0 - q[0] * q[0]); 143 | 144 | if (divisor > QUAT_DIVISION_EPSILON) 145 | { 146 | double inv_divisor = 1.0 / divisor; 147 | v[0] = q[1] * inv_divisor; 148 | v[1] = q[2] * inv_divisor; 149 | v[2] = q[3] * inv_divisor; 150 | } 151 | else // Infinite solutions 152 | { 153 | // Arbitrary axis 154 | v[0] = 1; 155 | v[1] = 0; 156 | v[2] = 0; 157 | 158 | status = false; 159 | } 160 | 161 | return status; 162 | } 163 | 164 | /** 165 | * @brief Computes the rate of change of the quaternion using the input quaternion and angular velocity. 166 | * 167 | * The angular velocity vector `w` is assumed to be expressed in the body frame. 168 | * 169 | * @param q Input quaternion 170 | * @param w Angular velocity vector in the body frame 171 | * @param q_dot Output rate of change of the quaternion 172 | */ 173 | void quat_rate(const double q[4], const double w[3], double q_dot[4]) 174 | { 175 | q_dot[0] = 0.5 * (-w[0] * q[1] - w[1] * q[2] - w[2] * q[3]); 176 | q_dot[1] = 0.5 * ( w[0] * q[0] + w[2] * q[2] - w[1] * q[3]); 177 | q_dot[2] = 0.5 * ( w[1] * q[0] - w[2] * q[1] + w[0] * q[3]); 178 | q_dot[3] = 0.5 * ( w[2] * q[0] + w[1] * q[1] - w[0] * q[2]); 179 | } 180 | 181 | 182 | // Rotates the input vector `v` using the quaternion `q` to produce the output vector `v_out`. 183 | void quat_rotate(const double q[4], const double v[3], double v_out[3]) 184 | { 185 | double q0_sq = q[0] * q[0]; 186 | double q1_sq = q[1] * q[1]; 187 | double q2_sq = q[2] * q[2]; 188 | double q3_sq = q[3] * q[3]; 189 | double q0q1x2 = 2.0 * q[0] * q[1]; 190 | double q0q2x2 = 2.0 * q[0] * q[2]; 191 | double q0q3x2 = 2.0 * q[0] * q[3]; 192 | double q1q2x2 = 2.0 * q[1] * q[2]; 193 | double q1q3x2 = 2.0 * q[1] * q[3]; 194 | double q2q3x2 = 2.0 * q[2] * q[3]; 195 | 196 | v_out[0] = (q0_sq + q1_sq - q2_sq - q3_sq) * v[0] + (q1q2x2 - q0q3x2) * v[1] + (q1q3x2 + q0q2x2) * v[2]; 197 | v_out[1] = (q1q2x2 + q0q3x2) * v[0] + (q0_sq - q1_sq + q2_sq - q3_sq) * v[1] + (q2q3x2 - q0q1x2) * v[2]; 198 | v_out[2] = (q1q3x2 - q0q2x2) * v[0] + (q2q3x2 + q0q1x2) * v[1] + (q0_sq - q1_sq - q2_sq + q3_sq) * v[2]; 199 | } 200 | 201 | // Computes the DCM `m` corresponding to the input quaternion `q`. 202 | void quat_to_dcm(const double q[4], double m[3][3]) 203 | { 204 | m[0][0] = q[0] * q[0] + q[1] * q[1] - q[2] * q[2] - q[3] * q[3]; 205 | m[0][1] = 2.0 * (q[1] * q[2] + q[0] * q[3]); 206 | m[0][2] = 2.0 * (q[1] * q[3] - q[0] * q[2]); 207 | m[1][0] = 2.0 * (q[1] * q[2] - q[0] * q[3]); 208 | m[1][1] = q[0] * q[0] - q[1] * q[1] + q[2] * q[2] - q[3] * q[3]; 209 | m[1][2] = 2.0 * (q[2] * q[3] + q[0] * q[1]); 210 | m[2][0] = 2.0 * (q[1] * q[3] + q[0] * q[2]); 211 | m[2][1] = 2.0 * (q[2] * q[3] - q[0] * q[1]); 212 | m[2][2] = q[0] * q[0] - q[1] * q[1] - q[2] * q[2] + q[3] * q[3]; 213 | } 214 | 215 | // Quaternion to intrinsic Euler angles as described in Ref.[2]. 216 | void quat_to_euler(const double q[4], double e[3], const euler_seq_t es) 217 | { 218 | // Parse Euler sequence 219 | const uint8_t i = (es / 100) % 10; 220 | const uint8_t j = (es / 10) % 10; 221 | uint8_t k = es % 10; 222 | 223 | // Tait-Bryan angles 224 | bool not_proper = true; 225 | 226 | // Proper Euler angles 227 | if (i == k) 228 | { 229 | k = 6 - i - j; 230 | not_proper = false; 231 | } 232 | 233 | // Is permutation even or odd? 234 | const int epsilon = -(i - j) * (j - k) * (k - i) / 2.0f; 235 | double a, b, c, d; 236 | 237 | if (not_proper) 238 | { 239 | a = q[0] - q[j]; 240 | b = q[i] + q[k] * epsilon; 241 | c = q[j] + q[0]; 242 | d = q[k] * epsilon - q[i]; 243 | } 244 | else 245 | { 246 | a = q[0]; 247 | b = q[i]; 248 | c = q[j]; 249 | d = q[k] * epsilon; 250 | } 251 | 252 | const double a_sq = a * a; 253 | const double b_sq = b * b; 254 | const double c_sq = c * c; 255 | const double d_sq = d * d; 256 | const double hyp_ab = a_sq + b_sq; 257 | const double hyp_cd = c_sq + d_sq; 258 | 259 | e[1] = acos(2.0 * ((hyp_ab) / (hyp_ab + hyp_cd)) - 1.0); 260 | const double theta_plus = atan2(b, a); 261 | const double theta_minus = atan2(d, c); 262 | 263 | // Check singularity 264 | if (fabs(e[1]) < EULER_SINGULARITY_EPSILON) 265 | { 266 | e[0] = QUAT_ANGLE_ON_SINGULARITY_RAD; 267 | e[2] = 2 * theta_plus - QUAT_ANGLE_ON_SINGULARITY_RAD; 268 | } 269 | else if (fabs(fabs(e[1]) - PI_2) < EULER_SINGULARITY_EPSILON) 270 | { 271 | e[0] = QUAT_ANGLE_ON_SINGULARITY_RAD; 272 | e[2] = 2 * theta_minus + QUAT_ANGLE_ON_SINGULARITY_RAD; 273 | } 274 | else // Safe 275 | { 276 | e[0] = theta_plus - theta_minus; 277 | e[2] = theta_plus + theta_minus; 278 | } 279 | 280 | if (not_proper) 281 | { 282 | e[1] -= PI_2; 283 | e[2] *= epsilon; 284 | } 285 | 286 | // Normalize to [-pi, pi] 287 | for (uint8_t i = 0; i < 3; i++) 288 | { 289 | if (e[i] < -PI) 290 | { 291 | e[i] += 2.0 * PI; 292 | } 293 | else if (e[i] > PI) 294 | { 295 | e[i] -= 2 * PI; 296 | } 297 | } 298 | } 299 | 300 | // Weighted average of two quaternions (n=2 case) as described in Ref.[7]. 301 | void quat_mean(const double *q[], const int n, const double *w, double qm[4]) 302 | { 303 | if (n != 2) 304 | { 305 | // TODO 306 | return; 307 | } 308 | 309 | const double *q1 = q[0]; 310 | const double *q2 = q[1]; 311 | const double w1 = w[0]; 312 | const double w2 = w[1]; 313 | 314 | double dot12 = quat_dot(q1, q2); 315 | double dot11 = quat_dot(q1, q1); 316 | double z = sqrt((w1 - w2) * (w1 - w2) + 4.0 * w1 * w2 * dot11 * dot11); // Eqn.(18) 317 | 318 | // Eqn.(19) 319 | double temp = z * (w1 + w2 + z); 320 | double a = sqrt(w1 * (w1 - w2 + z) / temp); 321 | double b = sqrt(w2 * (w2 - w1 + z) / temp); 322 | double sign = (dot12 >= 0) ? 1.0 : -1.0; 323 | 324 | for (int i = 0; i < 4; i++) 325 | { 326 | qm[i] = a * q1[i] + sign * b * q2[i]; 327 | } 328 | 329 | quat_normalize(qm); 330 | quat_canonize(qm); 331 | } 332 | 333 | /** 334 | * @brief Spherical linear interpolation (SLERP) between two quaternions. 335 | * 336 | * @param q1 Start quaternion (must be a unit quaternion) 337 | * @param q2 End quaternion (must be a unit quaternion) 338 | * @param u Interpolation parameter in range [0,1] 339 | * @param q Interpolated output quaternion 340 | * 341 | * @note When q1 and q2 are antipodal (q1 = -q2), the interpolation plane is chosen arbitrarily due 342 | * to infinite possible solutions. 343 | * 344 | * The parameter `u` determines the interpolation between q1 and q2. u = 0 and u = 1 returns 345 | * exactly q1 and q2 (or its antipodal equivalent -q2), respectively. Intermediate values 346 | * (e.g. u = 0.6) results in a quaternion representing 60% of the rotation from q1 toward q2 347 | * along the shortest path. 348 | */ 349 | void quat_slerp(const double q1[4], const double q2[4], const double u, double q[4]) 350 | { 351 | // If q1 and q2 represent same orientation. 352 | if(quat_equal(q1, q2)) 353 | { 354 | quat_copy(q1, q); 355 | return; 356 | } 357 | 358 | double qb[4] = {q2[0], q2[1], q2[2], q2[3]}; 359 | double dot = quat_dot(q1, q2); 360 | 361 | // Ensure shortest angular path 362 | if (dot < 0.0) 363 | { 364 | quat_neg(qb); 365 | dot = -dot; 366 | } 367 | 368 | // Ensure dot is in valid domain for acos i.e. [-1, 1]. 369 | dot = (dot < -1.0) ? -1.0 : (dot > 1.0) ? 1.0 : dot; 370 | 371 | const double theta = acos(fabs(dot)); 372 | const double st = sqrt(1.0 - dot * dot); 373 | double a, b; 374 | 375 | // Fall back to LERP if sin(theta) -> 0. 376 | if (st <= QUAT_DIVISION_EPSILON) 377 | { 378 | a = 1.0 - u; 379 | b = u; 380 | } 381 | else // SLERP 382 | { 383 | const double stinv = 1 / st; 384 | const double ut = u * theta; 385 | 386 | a = sin(theta - ut) * stinv; 387 | b = sin(ut) * stinv; 388 | } 389 | 390 | q[0] = a * q1[0] + b * qb[0]; 391 | q[1] = a * q1[1] + b * qb[1]; 392 | q[2] = a * q1[2] + b * qb[2]; 393 | q[3] = a * q1[3] + b * qb[3]; 394 | 395 | quat_normalize(q); 396 | } 397 | -------------------------------------------------------------------------------- /attitude/quat.h: -------------------------------------------------------------------------------- 1 | // Basic quaternion manipulations. 2 | // 2024-12-22 3 | 4 | #ifndef _ATTITUDE_QUAT_H_ 5 | #define _ATTITUDE_QUAT_H_ 6 | 7 | #include 8 | #include "euler.h" 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | void quat_neg(double q[4]); 15 | void quat_unit(double q[4]); 16 | void quat_canonize(double q[4]); 17 | void quat_normalize(double q[4]); 18 | double quat_norm(const double q[4]); 19 | void quat_to_dcm(const double q[4], double m[3][3]); 20 | void quat_conj(const double q[4], double q_conj[4]); 21 | void quat_copy(const double q_src[4], double q_dest[4]); 22 | bool quat_equal(const double q1[4], const double q2[4]); 23 | double quat_dot(const double q1[4], const double q2[4]); 24 | double quat_dist(const double q1[4], const double q2[4]); 25 | bool quat_to_axan(const double q[4], double *psi, double v[3]); 26 | void quat_prod(const double q1[4], const double q2[4], double q[4]); 27 | void quat_err(const double q1[4], const double q2[4], double qe[4]); 28 | void quat_rate(const double q[4], const double w[3], double q_dot[4]); 29 | void quat_rotate(const double q[4], const double v[3], double v_out[3]); 30 | void quat_to_euler(const double q[4], double e[3], const euler_seq_t es); 31 | void quat_mean(const double *q[], const int n, const double *w, double qm[4]); 32 | void quat_slerp(const double q1[4], const double q2[4], const double u, double q[4]); 33 | 34 | #ifdef __cplusplus 35 | } 36 | #endif 37 | 38 | #endif // quat.h 39 | -------------------------------------------------------------------------------- /attitude/test_att.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "attitude.h" 4 | 5 | #define PI 3.14159265358979323846 6 | 7 | #define RED "\x1b[31m" 8 | #define GREEN "\x1b[32m" 9 | #define RESET "\x1b[0m" 10 | 11 | // Error tolerance 12 | #define TOLERANCE 1e-9 13 | 14 | // Is the deviation within threshold? 15 | bool compare_matrices(double mat1[3][3], double mat2[3][3]) 16 | { 17 | for (int i = 0; i < 3; i++) 18 | { 19 | for (int j = 0; j < 3; j++) 20 | { 21 | if (fabs(mat1[i][j] - mat2[i][j]) > TOLERANCE) 22 | { 23 | return false; 24 | } 25 | } 26 | } 27 | 28 | return true; 29 | } 30 | 31 | bool compare_vectors(double vec1[3], double vec2[3]) 32 | { 33 | for (int i = 0; i < 3; i++) 34 | { 35 | if (fabs(vec1[i] - vec2[i]) > TOLERANCE) 36 | { 37 | return false; 38 | } 39 | } 40 | return true; 41 | } 42 | 43 | int main(void) 44 | { 45 | 46 | euler_seq_t es[] = 47 | { 48 | EULER_XYX, EULER_XYZ, EULER_XZX, EULER_XZY, 49 | EULER_YXY, EULER_YXZ, EULER_YZX, EULER_YZY, 50 | EULER_ZXY, EULER_ZXZ, EULER_ZYX, EULER_ZYZ 51 | }; 52 | 53 | double e[][3] = 54 | { 55 | { 0, 0, 0}, // Identity 56 | { PI / 2, PI / 2, PI / 2}, // Gimbal lock 57 | { -PI / 6, -PI / 2, PI / 4}, // Negative angle 58 | { 50 * PI / 7, 45 * PI / 3, 8 * PI}, // Large angles 59 | {-50 * PI / 7, 45 * PI / 3, -8 * PI}, // Negative and large angles 60 | 61 | // Random angles 62 | {0.433012701892219, -0.789149130992431, 0.435595740399158}, 63 | {0.75, 0.047367172745252, -0.659739608441123}, 64 | {0.5, 0.612372435695794, 0.612372435695794} 65 | }; 66 | 67 | double turth_dcm[][12][3][3] = 68 | { 69 | { 70 | // {0.000000, 0.000000, 0.000000} 71 | {{1, 0, -0}, {0, 1, 0}, {0, -0, 1}}, // XYX 72 | {{1, 0, 0}, {-0, 1, 0}, {0, -0, 1}}, // XYZ 73 | {{1, 0, 0}, {-0, 1, 0}, {0, -0, 1}}, // XZX 74 | {{1, 0, 0}, {-0, 1, 0}, {0, 0, 1}}, // XZY 75 | {{1, 0, -0}, {0, 1, 0}, {0, -0, 1}}, // YXY 76 | {{1, 0, 0}, {0, 1, 0}, {0, -0, 1}}, // YXZ 77 | {{1, 0, -0}, {0, 1, 0}, {0, -0, 1}}, // YZX 78 | {{1, 0, -0}, {-0, 1, 0}, {0, 0, 1}}, // YZY 79 | {{1, 0, -0}, {-0, 1, 0}, {0, 0, 1}}, // ZXY 80 | {{1, 0, 0}, {-0, 1, 0}, {0, -0, 1}}, // ZXZ 81 | {{1, 0, -0}, {0, 1, 0}, {0, 0, 1}}, // ZYX 82 | {{1, 0, -0}, {-0, 1, 0}, {0, 0, 1}}, // ZYZ 83 | }, 84 | 85 | { 86 | // {1.570796, 1.570796, 1.570796} 87 | {{6.123233996e-17, 1, -6.123233996e-17}, {1, -6.123233996e-17, 6.123233996e-17}, {6.123233996e-17, -6.123233996e-17, -1}}, // XYX 88 | {{3.749399457e-33, 1.224646799e-16, 1}, {-6.123233996e-17, -1, 1.224646799e-16}, {1, -6.123233996e-17, 3.749399457e-33}}, // XYZ 89 | {{6.123233996e-17, 6.123233996e-17, 1}, {-6.123233996e-17, -1, 6.123233996e-17}, {1, -6.123233996e-17, -6.123233996e-17}}, // XZX 90 | {{3.749399457e-33, 1, 0}, {-1, 3.749399457e-33, 6.123233996e-17}, {6.123233996e-17, 0, 1}}, // XZY 91 | {{-6.123233996e-17, 1, -6.123233996e-17}, {1, 6.123233996e-17, 6.123233996e-17}, {6.123233996e-17, -6.123233996e-17, -1}}, // YXY 92 | {{1, 6.123233996e-17, 0}, {0, 3.749399457e-33, 1}, {6.123233996e-17, -1, 3.749399457e-33}}, // YXZ 93 | {{3.749399457e-33, 1, -6.123233996e-17}, {1, 3.749399457e-33, 1.224646799e-16}, {1.224646799e-16, -6.123233996e-17, -1}}, // YZX 94 | {{-1, 6.123233996e-17, -6.123233996e-17}, {-6.123233996e-17, 6.123233996e-17, 1}, {6.123233996e-17, 1, -6.123233996e-17}}, // YZY 95 | {{-1, 1.224646799e-16, -6.123233996e-17}, {-6.123233996e-17, 3.749399457e-33, 1}, {1.224646799e-16, 1, 3.749399457e-33}}, // ZXY 96 | {{-6.123233996e-17, 6.123233996e-17, 1}, {-6.123233996e-17, -1, 6.123233996e-17}, {1, -6.123233996e-17, 6.123233996e-17}}, // ZXZ 97 | {{3.749399457e-33, 6.123233996e-17, -1}, {0, 1, 6.123233996e-17}, {1, 0, 3.749399457e-33}}, // ZYX 98 | {{-1, 6.123233996e-17, -6.123233996e-17}, {-6.123233996e-17, -6.123233996e-17, 1}, {6.123233996e-17, 1, 6.123233996e-17}}, // ZYZ 99 | }, 100 | 101 | { 102 | // {-0.523599, -1.570796, 0.785398} 103 | {{6.123233996e-17, 0.5, 0.8660254038}, {-0.7071067812, 0.6123724357, -0.3535533906}, {-0.7071067812, -0.6123724357, 0.3535533906}}, // XYX 104 | {{4.329780281e-17, 0.9659258263, 0.2588190451}, {-4.329780281e-17, 0.2588190451, -0.9659258263}, {-1, 3.061616998e-17, 5.302876194e-17}}, // XYZ 105 | {{6.123233996e-17, -0.8660254038, 0.5}, {0.7071067812, 0.3535533906, 0.6123724357}, {-0.7071067812, 0.3535533906, 0.6123724357}}, // XZX 106 | {{4.329780281e-17, -0.9659258263, -0.2588190451}, {1, 5.302876194e-17, -3.061616998e-17}, {4.329780281e-17, -0.2588190451, 0.9659258263}}, // XZY 107 | {{0.6123724357, -0.7071067812, 0.3535533906}, {0.5, 6.123233996e-17, -0.8660254038}, {0.6123724357, 0.7071067812, 0.3535533906}}, // YXY 108 | {{0.9659258263, 4.329780281e-17, -0.2588190451}, {-0.2588190451, 4.329780281e-17, -0.9659258263}, {-3.061616998e-17, 1, 5.302876194e-17}}, // YXZ 109 | {{5.302876194e-17, -1, 3.061616998e-17}, {0.2588190451, 4.329780281e-17, 0.9659258263}, {-0.9659258263, -4.329780281e-17, 0.2588190451}}, // YZX 110 | {{0.3535533906, -0.7071067812, -0.6123724357}, {0.8660254038, 6.123233996e-17, 0.5}, {-0.3535533906, -0.7071067812, 0.6123724357}}, // YZY 111 | {{0.2588190451, -0.9659258263, -4.329780281e-17}, {3.061616998e-17, 5.302876194e-17, -1}, {0.9659258263, 0.2588190451, 4.329780281e-17}}, // ZXY 112 | {{0.6123724357, -0.3535533906, -0.7071067812}, {-0.6123724357, 0.3535533906, -0.7071067812}, {0.5, 0.8660254038, 6.123233996e-17}}, // ZXZ 113 | {{5.302876194e-17, -3.061616998e-17, 1}, {-0.2588190451, 0.9659258263, 4.329780281e-17}, {-0.9659258263, -0.2588190451, 4.329780281e-17}}, // ZYX 114 | {{0.3535533906, 0.6123724357, 0.7071067812}, {0.3535533906, 0.6123724357, -0.7071067812}, {-0.8660254038, 0.5, 6.123233996e-17}}, // ZYZ 115 | }, 116 | 117 | { 118 | // {22.439948, 47.123890, 25.132741} 119 | {{-1, 7.444331965e-16, -1.545831461e-15}, {1.680943809e-30, -0.9009688679, -0.4338837391}, {-1.71574348e-15, -0.4338837391, 0.9009688679}}, // XYX 120 | {{-1, 1.627128109e-15, -1.120747995e-15}, {-9.797174393e-16, -0.9009688679, -0.4338837391}, {-1.71574348e-15, -0.4338837391, 0.9009688679}}, // XYZ 121 | {{-1, 1.545831461e-15, 7.444331965e-16}, {1.71574348e-15, 0.9009688679, 0.4338837391}, {1.680943809e-30, 0.4338837391, -0.9009688679}}, // XZX 122 | {{-1, 1.970914927e-15, -1.382617157e-16}, {1.71574348e-15, 0.9009688679, 0.4338837391}, {9.797174393e-16, 0.4338837391, -0.9009688679}}, // XZY 123 | {{-0.9009688679, 1.680943809e-30, 0.4338837391}, {7.444331965e-16, -1, 1.545831461e-15}, {0.4338837391, 1.71574348e-15, 0.9009688679}}, // YXY 124 | {{-0.9009688679, 9.797174393e-16, 0.4338837391}, {-1.382617157e-16, -1, 1.970914927e-15}, {0.4338837391, 1.71574348e-15, 0.9009688679}}, // YXZ 125 | {{0.9009688679, -1.71574348e-15, -0.4338837391}, {-1.120747995e-15, -1, 1.627128109e-15}, {-0.4338837391, -9.797174393e-16, -0.9009688679}}, // YZX 126 | {{0.9009688679, -1.71574348e-15, -0.4338837391}, {-1.545831461e-15, -1, 7.444331965e-16}, {-0.4338837391, 1.680943809e-30, -0.9009688679}}, // YZY 127 | {{-0.9009688679, -0.4338837391, -9.797174393e-16}, {-0.4338837391, 0.9009688679, -1.71574348e-15}, {1.627128109e-15, -1.120747995e-15, -1}}, // ZXY 128 | {{-0.9009688679, -0.4338837391, 1.680943809e-30}, {-0.4338837391, 0.9009688679, -1.71574348e-15}, {7.444331965e-16, -1.545831461e-15, -1}}, // ZXZ 129 | {{0.9009688679, 0.4338837391, 1.71574348e-15}, {0.4338837391, -0.9009688679, 9.797174393e-16}, {1.970914927e-15, -1.382617157e-16, -1}}, // ZYX 130 | {{0.9009688679, 0.4338837391, 1.71574348e-15}, {0.4338837391, -0.9009688679, 1.680943809e-30}, {1.545831461e-15, 7.444331965e-16, -1}}, // ZYZ 131 | }, 132 | 133 | { 134 | // {-22.439948, 47.123890, -25.132741} 135 | {{-1, -7.444331965e-16, -1.545831461e-15}, {-1.680943809e-30, -0.9009688679, 0.4338837391}, {-1.71574348e-15, 0.4338837391, 0.9009688679}}, // XYX 136 | {{-1, -1.627128109e-15, -1.120747995e-15}, {9.797174393e-16, -0.9009688679, 0.4338837391}, {-1.71574348e-15, 0.4338837391, 0.9009688679}}, // XYZ 137 | {{-1, 1.545831461e-15, -7.444331965e-16}, {1.71574348e-15, 0.9009688679, -0.4338837391}, {-1.680943809e-30, -0.4338837391, -0.9009688679}}, // XZX 138 | {{-1, 1.970914927e-15, 1.382617157e-16}, {1.71574348e-15, 0.9009688679, -0.4338837391}, {-9.797174393e-16, -0.4338837391, -0.9009688679}}, // XZY 139 | {{-0.9009688679, -1.680943809e-30, -0.4338837391}, {-7.444331965e-16, -1, 1.545831461e-15}, {-0.4338837391, 1.71574348e-15, 0.9009688679}}, // YXY 140 | {{-0.9009688679, -9.797174393e-16, -0.4338837391}, {1.382617157e-16, -1, 1.970914927e-15}, {-0.4338837391, 1.71574348e-15, 0.9009688679}}, // YXZ 141 | {{0.9009688679, -1.71574348e-15, 0.4338837391}, {-1.120747995e-15, -1, -1.627128109e-15}, {0.4338837391, 9.797174393e-16, -0.9009688679}}, // YZX 142 | {{0.9009688679, -1.71574348e-15, 0.4338837391}, {-1.545831461e-15, -1, -7.444331965e-16}, {0.4338837391, -1.680943809e-30, -0.9009688679}}, // YZY 143 | {{-0.9009688679, 0.4338837391, 9.797174393e-16}, {0.4338837391, 0.9009688679, -1.71574348e-15}, {-1.627128109e-15, -1.120747995e-15, -1}}, // ZXY 144 | {{-0.9009688679, 0.4338837391, -1.680943809e-30}, {0.4338837391, 0.9009688679, -1.71574348e-15}, {-7.444331965e-16, -1.545831461e-15, -1}}, // ZXZ 145 | {{0.9009688679, -0.4338837391, 1.71574348e-15}, {-0.4338837391, -0.9009688679, -9.797174393e-16}, {1.970914927e-15, 1.382617157e-16, -1}}, // ZYX 146 | {{0.9009688679, -0.4338837391, 1.71574348e-15}, {-0.4338837391, -0.9009688679, -1.680943809e-30}, {1.545831461e-15, -7.444331965e-16, -1}}, // ZYZ 147 | }, 148 | 149 | { 150 | // {0.433013, -0.789149, 0.435596} 151 | {{0.7044494784, -0.2978180513, 0.6442478876}, {-0.299481173, 0.6982178044, 0.6502329772}, {-0.6434764638, -0.6509963947, 0.4026682688}}, // XYX 152 | {{0.638667162, 0.1129995065, 0.761140833}, {-0.29724287, 0.9486075983, 0.1085831506}, {-0.7097541352, -0.2955921783, 0.6394328203}}, // XYZ 153 | {{0.7044494784, -0.6442478876, -0.2978180513}, {0.6434764638, 0.4026682688, 0.6509963947}, {-0.299481173, -0.6502329772, 0.6982178044}}, // XZX 154 | {{0.638667162, -0.4070336995, -0.6530144131}, {0.7097541352, 0.6394328203, 0.2955921783}, {0.29724287, -0.6522646977, 0.697278596}}, // XZY 155 | {{0.6982178044, -0.299481173, -0.6502329772}, {-0.2978180513, 0.7044494784, -0.6442478876}, {0.6509963947, 0.6434764638, 0.4026682688}}, // YXY 156 | {{0.697278596, 0.29724287, -0.6522646977}, {-0.6530144131, 0.638667162, -0.4070336995}, {0.2955921783, 0.7097541352, 0.6394328203}}, // YXZ 157 | {{0.6394328203, -0.7097541352, -0.2955921783}, {0.761140833, 0.638667162, 0.1129995065}, {0.1085831506, -0.29724287, 0.9486075983}}, // YZX 158 | {{0.4026682688, -0.6434764638, -0.6509963947}, {0.6442478876, 0.7044494784, -0.2978180513}, {0.6502329772, -0.299481173, 0.6982178044}}, // YZY 159 | {{0.9486075983, 0.1085831506, -0.29724287}, {-0.2955921783, 0.6394328203, -0.7097541352}, {0.1129995065, 0.761140833, 0.638667162}}, // ZXY 160 | {{0.6982178044, 0.6502329772, -0.299481173}, {-0.6509963947, 0.4026682688, -0.6434764638}, {-0.2978180513, 0.6442478876, 0.7044494784}}, // ZXZ 161 | {{0.6394328203, 0.2955921783, 0.7097541352}, {-0.6522646977, 0.697278596, 0.29724287}, {-0.4070336995, -0.6530144131, 0.638667162}}, // ZYX 162 | {{0.4026682688, 0.6509963947, 0.6434764638}, {-0.6502329772, 0.6982178044, -0.299481173}, {-0.6442478876, -0.2978180513, 0.7044494784}}, // ZYZ 163 | }, 164 | 165 | { 166 | // {0.750000, 0.047367, -0.659740} 167 | {{0.9988783852, 0.03227522869, -0.03464507442}, {-0.02902101208, 0.9954607029, 0.09064088348}, {0.03741326538, -0.0895337842, 0.9952808393}}, // XYX 168 | {{0.7892656092, -0.4229579151, -0.4451588483}, {0.6122236737, 0.5979271638, 0.5173637793}, {0.04734946218, -0.6808742239, 0.7308681958}}, // XYZ 169 | {{0.9988783852, 0.03464507442, 0.03227522869}, {-0.03741326538, 0.9952808393, 0.0895337842}, {-0.02902101208, -0.09064088348, 0.9954607029}}, // XZX 170 | {{0.7892656092, -0.3904091087, 0.4739625788}, {-0.04734946218, 0.7308681958, 0.6808742239}, {-0.6122236737, -0.5598324823, 0.5583634705}}, // XZY 171 | {{0.9954607029, -0.02902101208, -0.09064088348}, {0.03227522869, 0.9988783852, 0.03464507442}, {0.0895337842, -0.03741326538, 0.9952808393}}, // YXY 172 | {{0.5583634705, -0.6122236737, -0.5598324823}, {0.4739625788, 0.7892656092, -0.3904091087}, {0.6808742239, -0.04734946218, 0.7308681958}}, // YXZ 173 | {{0.7308681958, 0.04734946218, -0.6808742239}, {-0.4451588483, 0.7892656092, -0.4229579151}, {0.5173637793, 0.6122236737, 0.5979271638}}, // YZX 174 | {{0.9952808393, 0.03741326538, -0.0895337842}, {-0.03464507442, 0.9988783852, 0.03227522869}, {0.09064088348, -0.02902101208, 0.9954607029}}, // YZY 175 | {{0.5979271638, 0.5173637793, 0.6122236737}, {-0.6808742239, 0.7308681958, 0.04734946218}, {-0.4229579151, -0.4451588483, 0.7892656092}}, // ZXY 176 | {{0.9954607029, 0.09064088348, -0.02902101208}, {-0.0895337842, 0.9952808393, 0.03741326538}, {0.03227522869, -0.03464507442, 0.9988783852}}, // ZXZ 177 | {{0.7308681958, 0.6808742239, -0.04734946218}, {-0.5598324823, 0.5583634705, -0.6122236737}, {-0.3904091087, 0.4739625788, 0.7892656092}}, // ZYX 178 | {{0.9952808393, 0.0895337842, -0.03741326538}, {-0.09064088348, 0.9954607029, -0.02902101208}, {0.03464507442, 0.03227522869, 0.9988783852}}, // ZYZ 179 | }, 180 | 181 | { 182 | // {0.500000, 0.612372, 0.612372} 183 | {{0.8182866212, 0.2755787896, -0.5044435907}, {0.3304070055, 0.4926116328, 0.8050869456}, {0.4703596669, -0.8254635728, 0.3120443459}}, // XYX 184 | {{0.6695929945, 0.7299460274, -0.1372006519}, {-0.4703596669, 0.5597085129, 0.6822669305}, {0.5748104083, -0.3923075041, 0.7181140694}}, // XYZ 185 | {{0.8182866212, 0.5044435907, 0.2755787896}, {-0.4703596669, 0.3120443459, 0.8254635728}, {0.3304070055, -0.8050869456, 0.4926116328}}, // XZX 186 | {{0.6695929945, 0.6883582311, -0.2789411541}, {-0.5748104083, 0.7181140694, 0.3923075041}, {0.4703596669, -0.1023480778, 0.876519626}}, // XZY 187 | {{0.4926116328, 0.3304070055, -0.8050869456}, {0.2755787896, 0.8182866212, 0.5044435907}, {0.8254635728, -0.4703596669, 0.3120443459}}, // YXY 188 | {{0.876519626, 0.4703596669, -0.1023480778}, {-0.2789411541, 0.6695929945, 0.6883582311}, {0.3923075041, -0.5748104083, 0.7181140694}}, // YXZ 189 | {{0.7181140694, 0.5748104083, -0.3923075041}, {-0.1372006519, 0.6695929945, 0.7299460274}, {0.6822669305, -0.4703596669, 0.5597085129}}, // YZX 190 | {{0.3120443459, 0.4703596669, -0.8254635728}, {-0.5044435907, 0.8182866212, 0.2755787896}, {0.8050869456, 0.3304070055, 0.4926116328}}, // YZY 191 | {{0.5597085129, 0.6822669305, -0.4703596669}, {-0.3923075041, 0.7181140694, 0.5748104083}, {0.7299460274, -0.1372006519, 0.6695929945}}, // ZXY 192 | {{0.4926116328, 0.8050869456, 0.3304070055}, {-0.8254635728, 0.3120443459, 0.4703596669}, {0.2755787896, -0.5044435907, 0.8182866212}}, // ZXZ 193 | {{0.7181140694, 0.3923075041, -0.5748104083}, {-0.1023480778, 0.876519626, 0.4703596669}, {0.6883582311, -0.2789411541, 0.6695929945}}, // ZYX 194 | {{0.3120443459, 0.8254635728, -0.4703596669}, {-0.8050869456, 0.4926116328, 0.3304070055}, {0.5044435907, 0.2755787896, 0.8182866212}}, // ZYZ 195 | } 196 | }; 197 | 198 | int count = 0; 199 | 200 | for (int i = 0; i < 8; i++) 201 | { 202 | for (int j = 0; j < 12; j ++) 203 | { 204 | double dcm[3][3], euler[3]; 205 | euler_to_dcm(e[i], es[j], dcm); 206 | dcm_to_euler(dcm, euler, es[j]); 207 | 208 | if(compare_matrices(turth_dcm[i][j], dcm)) 209 | { 210 | printf(GREEN "euler_to_dcm: %d/95 PASSED! ", count); 211 | } 212 | else 213 | { 214 | printf(RED "euler_to_dcm: %d/95 FAILED. ", count); 215 | } 216 | 217 | if(compare_vectors(euler, e[i])) 218 | { 219 | printf(GREEN "dcm_to_euler: %d/95 PASSED!\n", count); 220 | } 221 | else 222 | { 223 | printf(RED "dcm_to_euler: %d/95 FAILED.\n", count); 224 | } 225 | 226 | count ++; 227 | } 228 | } 229 | 230 | printf(RESET); 231 | 232 | // 0.174461059412466 + 0.595055005141286i - 0.732945697135043j + 0.279756116387487k 233 | // 0.219081163106642 - 0.898748787861072i - 0.297999382249188j + 0.235479146569706k 234 | double q1[4] = {0.174461059412466, 0.595055005141286, 0.732945697135043, 0.279756116387487}; 235 | double q2[4] = {0.219081163106642, -0.898748787861072, 0.297999382249188, 0.235479146569706}; 236 | double u = 0.3; 237 | 238 | double q[4]; 239 | quat_slerp(q1, q2, u, q); 240 | 241 | // 0.0564395763769562 + 0.859378789689511i - 0.489280556589716j + 0.137430735257576k 242 | printf("q-slerp: %.15f, %.15f, %.15f, %.15f\n", q[0], q[1], q[2], q[3]); 243 | 244 | // q1 = 0.637500352868806 + 0.49338212432488i - 0.240485730112637j + 0.54067919610524k 245 | // q2 = 0.697035155263178 - 0.0605889265769267i + 0.686790776520073j - 0.19695025663971k 246 | // qe = 2.854979893415346 247 | double q3[4] = {0.637500352868806, 0.49338212432488, -0.240485730112637, 0.54067919610524}; 248 | double q4[4] = {0.697035155263178, -0.0605889265769267, 0.686790776520073, -0.19695025663971}; 249 | 250 | printf("q-dist: %.15f\n", quat_dist(q4, q3)); 251 | 252 | const double *qin[2] = {q3, q4}; // Array of pointers 253 | double w[2] = {0.23, 0.32}; 254 | double qm[4]; 255 | quat_mean(qin, 2, w, qm); 256 | printf("q-mean: %.15f, %.15f, %.15f, %.15f\n", qm[0], qm[1], qm[2], qm[3]); 257 | 258 | return 0; 259 | } 260 | -------------------------------------------------------------------------------- /igrf/README.md: -------------------------------------------------------------------------------- 1 | # IGRF 2 | 3 | International Geomagnetic Reference Field (IGRF-14) Model. 4 | 5 | ## References 6 | 7 | 1. Davis - Mathematical Modeling of Earth's Magnetic Field (2004) 8 | 2. Yang - Spacecraft Modeling Attitude Determination and Control (2019) 9 | 3. [IGRF-14](https://doi.org/10.5281/zenodo.14218973) - International Association of Geomagnetism and Aeronomy (2024) 10 | 4. Wertz (ed.) - Spacecraft Attitude Determination and Control (1980) 11 | 5. Barton, Tarlowski - Geomagnetic, geocentric, and geodetic coordinate transformations (1991) 12 | -------------------------------------------------------------------------------- /igrf/c/Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | CFLAGS = -std=c99 -Wall -Wextra -Werror -pedantic -fsanitize=undefined,address -lm 3 | 4 | TARGET = igrf 5 | BUILD_DIR = build 6 | 7 | INC = \ 8 | 9 | SRC = \ 10 | igrf.c \ 11 | main.c 12 | 13 | all: $(TARGET) 14 | 15 | $(TARGET): $(SRC) 16 | @mkdir -p $(BUILD_DIR) 17 | $(CC) $(CFLAGS) $(INC) -o $(BUILD_DIR)/$(TARGET) $(SRC) 18 | 19 | run: clean all 20 | ./$(BUILD_DIR)/$(TARGET) 21 | 22 | clean: 23 | rm -rf $(BUILD_DIR) 24 | 25 | .PHONY: all clean 26 | -------------------------------------------------------------------------------- /igrf/c/igrf.c: -------------------------------------------------------------------------------- 1 | #include "igrf.h" 2 | #include 3 | 4 | // World Geodetic System 1984 (WGS84) params 5 | static const double wgs84_f = 1 / 298.257223563; 6 | static const double wgs84_a = 6378.137; 7 | static const double wgs84_b = wgs84_a * (1 - wgs84_f); 8 | 9 | // Decimal years since January 1, IGRF_START_YEAR 10 | double get_years(const igrf_time_t dt) 11 | { 12 | if ((dt.year < IGRF_START_YEAR) || (dt.year >= IGRF_END_YEAR) || 13 | (dt.month < 1) || (dt.month > 12) || 14 | (dt.day < 1) || (dt.hour > 23) || 15 | (dt.minute > 59) || (dt.second > 59)) 16 | { 17 | return -1; 18 | } 19 | 20 | int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; 21 | int is_leap = (((dt.year % 4) == 0) && (((dt.year % 100) != 0) || ((dt.year % 400) == 0))); 22 | 23 | // Adjust for leap year 24 | if (is_leap) 25 | { 26 | days_in_month[1] = 29; 27 | } 28 | 29 | // Check valid day in the month 30 | if (dt.day > days_in_month[dt.month - 1]) 31 | { 32 | return -1; 33 | } 34 | 35 | // Days since IGRF_START_YEAR 36 | int years = dt.year - IGRF_START_YEAR; 37 | int days_arr[] = {0, 31, 59, 90, 120, 151, 182, 212, 243, 273, 304, 334}; 38 | double days = days_arr[dt.month - 1] + dt.day + (dt.month > 2 ? is_leap : 0); 39 | double hours = dt.hour + (dt.minute / 60.0) + (dt.second / 3600.0); 40 | int total_days = is_leap ? 366 : 365; 41 | 42 | // Decimal years 43 | return years + days / total_days + hours / 24.0; 44 | } 45 | 46 | /** 47 | * @brief Computes magnetic field strength [nT] in specified coordinate frame. 48 | * 49 | * @param t Time between January 1, IGRF_START_YEAR and December 31, IGRF_END_YEAR. 50 | * @param x Spatial coordinates: {latitude [deg], longitude [deg], altitude [km]}. 51 | * - For geodetic frame, altitude represents height above Earth's surface. 52 | * - For geocentric frame, altitude represents radius from the center of the Earth. 53 | * @param b Output magnetic field intensity in specified frame. 54 | * @param f The frame of reference for the input coordinates and the output field values. 55 | * 56 | * @return false if the time is out of range; true otherwise. 57 | */ 58 | bool igrf(const igrf_time_t t, const double x[3], const igrf_frame_t f, double b[3]) 59 | { 60 | const double a = 6371.2; // Radius of Earth [km] 61 | const double theta = M_PI_2 - x[0] * D2R; // Colattitude [rad] 62 | const double phi = x[1] * D2R; // Longitude [rad] 63 | 64 | double cd, sd, r; 65 | double ct = cos(theta); 66 | double st = sin(theta); 67 | 68 | // Geodetic to geocentric conversion 69 | // https://github.com/wb-bgs/m_IGRF 70 | if (f == IGRF_GEODETIC) 71 | { 72 | // Radius 73 | const double h = x[2]; 74 | const double rho = hypot(wgs84_a * st, wgs84_b * ct); 75 | r = sqrt(h * h + 2 * h * rho + (pow(wgs84_a, 4) * st * st + pow(wgs84_b, 4) * ct * ct) / (rho * rho)); 76 | 77 | // Latitude 78 | cd = (h + rho) / r; 79 | sd = (wgs84_a * wgs84_a - wgs84_b * wgs84_b) / rho * ct * st / r; 80 | const double temp = ct; 81 | ct = cd * ct - sd * st; 82 | st = cd * st + sd * temp; 83 | } 84 | else if (f == IGRF_GEOCENTRIC) 85 | { 86 | r = x[2]; // Radius 87 | } 88 | 89 | // Avoid singularity on pole 90 | const double epsilon = 1e-8; 91 | 92 | if (st < epsilon && st > -epsilon) 93 | { 94 | st = epsilon; 95 | } 96 | 97 | double years = get_years(t); 98 | 99 | if (years < 0) 100 | { 101 | return false; 102 | } 103 | 104 | // [a] Re-occurring power factors 105 | // Optimizations [a] and [b] by Alar Leibak. 106 | double ar_pow[IGRF_DEGREE + 1]; 107 | const double ar = a / r; 108 | ar_pow[0] = ar * ar * ar; 109 | 110 | for (uint8_t i = 1; i <= IGRF_DEGREE; i++) 111 | { 112 | ar_pow[i] = ar_pow[i - 1] * ar; 113 | } 114 | 115 | // [b] Re-occurring sines and cosines 116 | double sines[IGRF_DEGREE + 1], cosines[IGRF_DEGREE + 1]; 117 | sines[0] = 0; 118 | cosines[0] = 1; 119 | sines[1] = sin(phi); 120 | cosines[1] = cos(phi); 121 | 122 | for (uint8_t i = 2; i <= IGRF_DEGREE; i++) 123 | { 124 | if (i & 1) 125 | { 126 | sines[i] = sines[i - 1] * cosines[1] + cosines[i - 1] * sines[1]; 127 | cosines[i] = cosines[i - 1] * cosines[1] - sines[i - 1] * sines[1]; 128 | } 129 | else // even 130 | { 131 | sines[i] = 2 * sines[i >> 1] * cosines[i >> 1]; 132 | cosines[i] = 2 * cosines[i >> 1] * cosines[i >> 1] - 1; 133 | } 134 | } 135 | 136 | // Associated Legendre polynomials and its derivative 137 | double pnm = 0.0f, dpnm = 0.0f; // (n, m) 138 | double p11 = 1.0f, dp11 = 0.0f; // (n, n) 139 | double p10 = 1.0f, dp10 = 0.0f; // (n-1, m) 140 | double p20 = 0.0f, dp20 = 0.0f; // (n-2, m) 141 | 142 | // Field components: radial, theta, and phi 143 | double br = 0.0f, bt = 0.0f, bp = 0.0f; 144 | 145 | for (uint8_t m = 0; m <= IGRF_DEGREE; m++) 146 | { 147 | for (uint8_t n = 1; n <= IGRF_DEGREE; n++) 148 | { 149 | if (m <= n) 150 | { 151 | if (n == m) 152 | { 153 | pnm = st * p11; 154 | dpnm = st * dp11 + ct * p11; 155 | 156 | p11 = pnm; dp11 = dpnm; 157 | p20 = 0.0; dp20 = 0.0; 158 | } 159 | else 160 | { 161 | double Knm = 0.0; 162 | 163 | if (n > 1) 164 | { 165 | Knm = (pow(n - 1, 2) - pow(m, 2)) / ((2.0f * n - 1) * (2.0f * n - 3)); 166 | } 167 | 168 | pnm = ct * p10 - Knm * p20; 169 | dpnm = ct * dp10 - st * p10 - Knm * dp20; 170 | } 171 | 172 | p20 = p10; dp20 = dp10; 173 | p10 = pnm; dp10 = dpnm; 174 | 175 | // Linear interpolation of g and h 176 | const int k = (0.5 * n * (n + 1) + m) - 1; 177 | const double g = g_val[k] + g_sv[k] * years; 178 | const double h = h_val[k] + h_sv[k] * years; 179 | 180 | if (m == 0) 181 | { 182 | const double temp = ar_pow[n - 1] * g; 183 | br += temp * (n + 1.0) * pnm; 184 | bt -= temp * dpnm; 185 | } 186 | else 187 | { 188 | const double hsin = h * sines[m]; 189 | const double hcos = h * cosines[m]; 190 | const double gsin = g * sines[m]; 191 | const double gcos = g * cosines[m]; 192 | const double temp = ar_pow[n - 1] * (gcos + hsin); 193 | 194 | br += temp * (n + 1.0) * pnm; 195 | bt -= temp * dpnm; 196 | bp -= ar_pow[n - 1] * m * (-gsin + hcos) * pnm; 197 | } 198 | } 199 | } 200 | } 201 | 202 | bp = bp / st; 203 | 204 | // Geocentric NED 205 | b[0] = -bt; 206 | b[1] = bp; 207 | b[2] = -br; 208 | 209 | // Geocentric to geodetic NED 210 | if (f == IGRF_GEODETIC) 211 | { 212 | double temp = b[0]; 213 | b[0] = cd * b[0] + sd * b[2]; 214 | b[2] = cd * b[2] - sd * temp; 215 | } 216 | 217 | return true; 218 | } 219 | 220 | // Magnetic inclination [rad] 221 | double igrf_inc(const double b[3]) 222 | { 223 | const double h = sqrt(b[0] * b[0] + b[1] * b[1]); 224 | return atan(b[2] / h); 225 | } 226 | 227 | // Magnetic declination [rad] 228 | double igrf_dec(const double b[3]) 229 | { 230 | return atan2(b[1], b[0]); 231 | } 232 | 233 | // Magnitude of magnetic field 234 | double igrf_mag(const double b[3]) 235 | { 236 | return sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]); 237 | } 238 | -------------------------------------------------------------------------------- /igrf/c/igrf.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @brief Implementation of International Geomagnetic Reference Field (IGRF-14) Model. 3 | * @cite Davis - Mathematical Modeling of Earth's Magnetic Field (2004) 4 | * @author risherlock 5 | * @date 2021-12-26 6 | */ 7 | 8 | #ifndef _IGRF_H_ 9 | #define _IGRF_H_ 10 | 11 | #ifdef __cplusplus 12 | extern "C" 13 | { 14 | #endif 15 | 16 | #include "igrf_2025.h" 17 | 18 | #include 19 | #include 20 | 21 | #define R2D 57.2957795131 22 | #define D2R 0.01745329251 23 | #define M_PI_2 1.57079632679489661923 24 | 25 | typedef struct 26 | { 27 | uint16_t year; 28 | uint8_t month; 29 | uint8_t day; 30 | uint8_t hour; 31 | uint8_t minute; 32 | uint8_t second; 33 | } igrf_time_t; 34 | 35 | typedef enum 36 | { 37 | IGRF_GEODETIC, 38 | IGRF_GEOCENTRIC 39 | } igrf_frame_t; 40 | 41 | double igrf_mag(const double b[3]); 42 | double igrf_inc(const double b[3]); 43 | double igrf_dec(const double b[3]); 44 | bool igrf(const igrf_time_t t, const double x[3], const igrf_frame_t f, double b[3]); 45 | 46 | #ifdef __cplusplus 47 | } 48 | #endif 49 | 50 | #endif // igrf.h 51 | -------------------------------------------------------------------------------- /igrf/c/igrf_2025.h: -------------------------------------------------------------------------------- 1 | #ifndef _IGRF_2025_H_ 2 | #define _IGRF_2025_H_ 3 | 4 | #define IGRF_DEGREE 13 5 | #define IGRF_END_YEAR 2030 6 | #define IGRF_START_YEAR 2025 7 | 8 | // Schmidt quasi-normalized coefficients 9 | static const double g_val[] = {-29350.00000, -1410.30000, -3834.30000, 5111.10873, 1427.81608, 3402.25000, -7361.32905, 2408.60834, 358.44417, 3914.31250, 4424.97513, 218.35204, -587.96283, 8.87412, -1834.08750, 3751.46849, 1438.67189, -652.75169, -314.80940, 14.66262, 928.33125, 1206.01936, 1146.22262, -1152.69845, -223.18566, 34.66953, -40.83895, 2134.27500, -2727.61251, -254.85513, 1214.36824, 195.11311, 15.43616, -27.12444, 9.25582, 1161.31641, 730.64062, -981.44143, 82.83915, -582.84959, 250.63691, 102.29189, -42.11469, 0.62671, 446.31641, 1019.22773, 325.95012, -16.59657, -140.93935, -441.35141, 41.75503, 64.78831, -22.48061, -7.79583, -234.55352, -1557.03087, 42.13838, 330.56068, -116.87085, -36.95781, -37.18808, 30.06478, 7.36434, -6.90244, -2.31515, 1033.34766, -652.94103, -1022.61993, 787.12322, -143.70838, 0.00000, -56.47059, -4.96044, 25.03604, -8.81492, -0.27203, 1.50795, -1320.38867, -89.70275, 318.05194, 779.06498, -584.29873, 200.41281, 104.14946, 58.52694, -5.85269, -12.77163, -1.88649, -3.33778, -0.39744, 253.92090, -1557.16135, 928.51164, 898.19471, -196.82397, 347.93891, 45.15452, 186.99640, 0.00000, 20.42190, 5.67768, 4.01473, -1.41942, -0.22270}; 10 | static const double h_val[] = {0.00000, 4545.50000, 0.00000, -5427.55441, -705.11788, 0.00000, -174.21996, 460.11042, -434.49695, 0.00000, 1541.76847, -524.35794, 443.42981, -277.61204, 0.00000, 460.54613, 1690.74688, -578.39353, 95.17493, 74.50575, 0.00000, -347.81750, 251.06310, 487.18197, -326.32035, 25.36227, 48.89927, 0.00000, -1734.46361, -417.03566, -20.47839, 290.19988, -45.69104, -60.78780, -1.42397, 0.00000, 482.62500, -706.63783, 476.32509, -259.34133, 188.34845, 4.80566, -13.03550, 2.44416, 0.00000, -3159.60598, 1314.66550, 688.75757, -191.67751, -178.56202, 125.26510, -4.52011, 2.06718, 5.96868, 0.00000, 802.84404, 21.06919, 413.20085, 631.10259, -665.24054, 16.52803, -84.18138, -31.09387, 2.38931, -5.34265, 0.00000, 0.00000, 1145.33433, -196.78080, 23.95140, 79.21180, -28.23529, -59.52522, -38.69206, -25.56328, -4.89662, -1.33395, 0.00000, -1076.43295, 477.07792, 649.22081, -730.37341, 0.00000, 124.97935, -23.41078, 46.82155, 2.55433, -8.48922, 0.27815, 0.11355, 0.00000, -1557.16135, 1083.26357, 1539.76235, -295.23595, -904.64116, -45.15452, 53.42754, -28.55822, 34.03650, 17.03304, -6.02209, -0.85165, -0.27837}; 11 | 12 | // Secular variations 13 | static const double g_sv[] = {12.60000, 10.00000, -16.80000, -9.17987, -7.18801, -3.75000, -13.47219, 0.77460, -12.33288, -7.43750, -12.72817, -22.69609, 11.29491, -5.02867, 4.72500, 13.21656, 0.00000, 3.29435, 5.10262, 0.70156, -2.88750, -5.67094, 11.95539, 11.95539, -4.36549, 0.93073, 0.60452, -2.68125, -3.54696, -2.89608, 10.23919, -1.23489, -4.93957, -1.93746, 0.58253, -5.02734, 13.40625, 0.00000, 16.56783, -2.67362, 4.44918, 0.68652, 0.00000, 0.18801, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000}; 14 | static const double h_sv[] = {0.00000, -21.50000, 0.00000, -47.28499, -9.61288, 0.00000, 11.63508, -0.38730, -3.08322, 0.00000, -7.19418, 16.04379, 3.34664, -3.03199, 0.00000, -5.08329, 16.13895, 2.35311, 3.77150, 1.33297, 0.00000, 5.67094, -23.91077, -3.98513, 4.36549, 1.62877, 0.60452, 0.00000, 21.28176, 14.48040, -14.33487, 0.00000, -5.55702, 1.21091, -0.19418, 0.00000, -20.10938, 22.43295, -12.42587, 10.69449, -7.41529, -4.11914, 0.75205, 0.12534, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000, 0.00000}; 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /igrf/c/main.c: -------------------------------------------------------------------------------- 1 | // Use case of IGRF-14 implementation. 2 | // 2021-12-17 3 | 4 | #include 5 | #include "igrf.h" 6 | 7 | int main() 8 | { 9 | // Miss Violet Smith disturbed Holmes 130 years ago on this day 10 | const igrf_time_t dt = {.year = 2025, .month = 04, .day = 23, 0, 0, 0}; 11 | 12 | // Geodetic coordinates of 221B Baker Street on LEO 13 | const double latitude = 51.523788; // deg 14 | const double longitude = -0.158611; // deg 15 | const double altitude = 400.0; // km 16 | const double x[3] = {latitude, longitude, altitude}; 17 | 18 | // Magnetic field in NED frame 19 | double b[3] = {0.0}; 20 | bool status = igrf(dt, x, IGRF_GEODETIC, b); 21 | 22 | if (status) 23 | { 24 | printf("Inputs:\n"); 25 | printf(" Time : %d-%d-%d, %d:%d:%d\n", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second); 26 | printf(" Latitude : %f deg\n", latitude); 27 | printf(" Longitude : %f deg\n", longitude); 28 | printf(" Altitude : %f km\n", altitude); 29 | printf("\nOutputs:\n"); 30 | printf(" Bn : %f nT\n", b[0]); 31 | printf(" Be : %f nT\n", b[1]); 32 | printf(" Bd : %f nT\n", b[2]); 33 | printf(" Magnitude : %f nT\n", igrf_mag(b)); 34 | printf(" Inclination : %f deg\n", igrf_inc(b) * R2D); 35 | printf(" Declination : %f deg\n", igrf_dec(b) * R2D); 36 | } 37 | else 38 | { 39 | printf("Date error!\n"); 40 | } 41 | 42 | return 0; 43 | } 44 | 45 | /* 46 | Expected output: 47 | 48 | Inputs: 49 | Time : 2025-4-23, 0:0:0 50 | Latitude : 51.523788 deg 51 | Longitude : -0.158611 deg 52 | Altitude : 400.000000 km 53 | 54 | Outputs: 55 | Bn : 16625.026351 nT 56 | Be : 69.051911 nT 57 | Bd : 37532.201921 nT 58 | Magnitude : 41049.512182 nT 59 | Inclination : 66.108694 deg 60 | Declination : 0.237976 deg 61 | */ 62 | -------------------------------------------------------------------------------- /igrf/matlab/igrf.m: -------------------------------------------------------------------------------- 1 | function [Bn,Be,Bd] = igrf(theta,phi,r,days) 2 | % Calculates magnetic field strength in local spherical coordinates 3 | % 4 | % Inputs: 5 | % theta = Latitude measured in degrees positive from equator 6 | % phi = Longitude measured in degrees positive east from Greenwich 7 | % r = Geocentric radius, km 8 | % days = Decimal days since January 1, 2020 9 | % 10 | % Output: 11 | % Br, Bt, Bp = B in radial, theta and phi direction 12 | % Bn, Be, Bd = B in North, East and Down direction 13 | % 14 | % References: 15 | % [1] Davis - Mathematical Modeling of Earth's Magnetic Field (2004) 16 | % [2] Yaguang - Spacecraft Modeling Attitude Determination and Control: 17 | % Quaternion Based Approach (2019) 18 | % 19 | % Note: 20 | % Variable names used in this code follows reference [2]. 21 | % 22 | % Test: http://www.geomag.bgs.ac.uk/data_service/models_compass/igrf_calc.html 23 | % Latest IGRF model: https://www.ngdc.noaa.gov/IAGA/vmod/igrf.html 24 | % 25 | % Rishav (2020/5/25) 26 | 27 | % Check pole to avoid singularities 28 | if (theta>-0.00000001 && theta<0.00000001) 29 | theta = 0.00000001; 30 | elseif(theta<180.00000001 && theta>179.99999999) 31 | theta = 179.99999999; 32 | end 33 | 34 | deg2rad = pi/180; 35 | theta = (90-theta)*deg2rad; 36 | phi = phi*deg2rad; 37 | a = 6371.2; % Radius of Earth, Km 38 | 39 | % Read in the g and h Schmidt quasi-normalized coefficients 40 | norm_g = fopen('./igrf_coeffs/norm_g_2020.txt'); 41 | norm_h = fopen('./igrf_coeffs/norm_h_2020.txt'); 42 | read_h = textscan(norm_h,'%f %f %f %f'); 43 | read_g = textscan(norm_g,'%f %f %f %f'); 44 | 45 | % Unpack the file values to variables 46 | hn = read_h{1}; hm = read_h{2}; h_val = read_h{3}; h_svi = read_h{4}; 47 | gn = read_g{1}; gm = read_g{2}; g_val = read_g{3}; g_svi = read_g{4}; 48 | 49 | % Close files 50 | fclose(norm_g); 51 | fclose(norm_h); 52 | 53 | % Gauss coefficients 54 | N = max(gn); 55 | g = zeros(N,N+1); 56 | h = zeros(N,N+1); 57 | for x=1:length(gn) 58 | g(gn(x),gm(x)+1) = g_val(x) + g_svi(x)*days/365; 59 | h(hn(x),hm(x)+1) = h_val(x) + h_svi(x)*days/365; 60 | end 61 | 62 | % 1. Calculate Legendre polynomials and its derivatives recursively 63 | % 2. Compute magnetic field strength using terms computed in 1 64 | Br = 0; Bt = 0; Bp = 0; 65 | Pnpnp = 1; Pnpm = 1; 66 | dPnpnp = 0; dPnpm = 0; 67 | for m = 0:N 68 | for n = 1:N 69 | if m <= n 70 | if (n == m) 71 | % Eqn(5.24b) and (5.25b) ...[2] 72 | Pnm = sin(theta)*Pnpnp; 73 | dPnm = sin(theta)*dPnpnp + cos(theta)*Pnpnp; 74 | 75 | Pnpnp = Pnm; Pnpm = Pnpnp; Pnppm = 0; 76 | dPnpnp = dPnm; dPnpm = dPnpnp; dPnppm = 0; 77 | 78 | elseif (n == 1) 79 | % K = 0 80 | % Eqn(5.24c) and (5.25c) ...[2] 81 | Pnm = cos(theta)*Pnpm; 82 | dPnm = cos(theta)*dPnpm - sin(theta)*Pnpm; 83 | 84 | Pnppm = Pnpm; Pnpm = Pnm; 85 | dPnppm = dPnpm; dPnpm = dPnm; 86 | 87 | else % (n != m) && (n > 1) 88 | % Eqn(5.24e), (5.24c) and (5.25c) ...[2] 89 | Knm = ((n-1)^2-m^2)/((2*n-1)*(2*n-3)); 90 | Pnm = cos(theta)*Pnpm - Knm*Pnppm; 91 | dPnm = cos(theta)*dPnpm - sin(theta)*Pnpm - Knm*dPnppm; 92 | 93 | Pnppm = Pnpm; Pnpm = Pnm; 94 | dPnppm = dPnpm; dPnpm = dPnm; 95 | end 96 | 97 | % Eqn(5.14a), (5.14b) and (5.14c) ...[2] 98 | Br = Br + (a/r)^(n+2)*(n+1)*... 99 | ((g(n,m+1)*cos(m*phi) + h(n,m+1)*sin(m*phi))*Pnm); 100 | 101 | Bt = Bt + (a/r)^(n+2)*... 102 | ((g(n,m+1)*cos(m*phi) + h(n,m+1)*sin(m*phi))*dPnm); 103 | 104 | Bp = Bp + (a/r)^(n+2)*... 105 | (m*(-g(n,m+1)*sin(m*phi) + h(n,m+1)*cos(m*phi))* Pnm); 106 | end 107 | end 108 | end 109 | Bt = -Bt; 110 | Bp = -Bp/sin(theta); 111 | 112 | Bn = -Bt; 113 | Be = Bp; 114 | Bd = -Br; 115 | end 116 | -------------------------------------------------------------------------------- /igrf/matlab/igrf_coeffs/igrf_2020.txt: -------------------------------------------------------------------------------- 1 | g 1 0 -2.9405e+04 5.7000 2 | g 1 1 -1.4509e+03 7.4000 3 | h 1 1 4.6525e+03 -25.9000 4 | g 2 0 -2.4996e+03 -11 5 | g 2 1 2982 -7 6 | h 2 1 -2.9916e+03 -30.2000 7 | g 2 2 1677 -2.1000 8 | h 2 2 -734.6000 -22.4000 9 | g 3 0 1.3632e+03 2.2000 10 | g 3 1 -2.3812e+03 -5.9000 11 | h 3 1 -82.1000 6 12 | g 3 2 1.2362e+03 3.1000 13 | h 3 2 241.9000 -1.1000 14 | g 3 3 525.7000 -12 15 | h 3 3 -543.4000 0.5000 16 | g 4 0 903 -1.2000 17 | g 4 1 809.5000 -1.6000 18 | h 4 1 281.9000 -0.1000 19 | g 4 2 86.3000 -5.9000 20 | h 4 2 -158.4000 6.5000 21 | g 4 3 -309.4000 5.2000 22 | h 4 3 199.7000 3.6000 23 | g 4 4 48 -5.1000 24 | h 4 4 -349.7000 -5 25 | g 5 0 -234.3000 -0.3000 26 | g 5 1 363.2000 0.5000 27 | h 5 1 47.7000 0 28 | g 5 2 187.8000 -0.6000 29 | h 5 2 208.3000 2.5000 30 | g 5 3 -140.7000 0.2000 31 | h 5 3 -121.2000 -0.6000 32 | g 5 4 -151.2000 1.3000 33 | h 5 4 32.3000 3 34 | g 5 5 13.5000 0.9000 35 | h 5 5 98.9000 0.3000 36 | g 6 0 66 -0.5000 37 | g 6 1 65.5000 -0.3000 38 | h 6 1 -19.1000 0 39 | g 6 2 72.9000 0.4000 40 | h 6 2 25.1000 -1.6000 41 | g 6 3 -121.5000 1.3000 42 | h 6 3 52.8000 -1.3000 43 | g 6 4 -36.2000 -1.4000 44 | h 6 4 -64.5000 0.8000 45 | g 6 5 13.5000 0 46 | h 6 5 8.9000 0 47 | g 6 6 -64.7000 0.9000 48 | h 6 6 68.1000 1 49 | g 7 0 80.6000 -0.1000 50 | g 7 1 -76.7000 -0.2000 51 | h 7 1 -51.5000 0.6000 52 | g 7 2 -8.2000 0 53 | h 7 2 -16.9000 0.6000 54 | g 7 3 56.5000 0.7000 55 | h 7 3 2.2000 -0.8000 56 | g 7 4 15.8000 0.1000 57 | h 7 4 23.5000 -0.2000 58 | g 7 5 6.4000 -0.5000 59 | h 7 5 -2.2000 -1.1000 60 | g 7 6 -7.2000 -0.8000 61 | h 7 6 -27.2000 0.1000 62 | g 7 7 9.8000 0.8000 63 | h 7 7 -1.8000 0.3000 64 | g 8 0 23.7000 0 65 | g 8 1 9.7000 0.1000 66 | h 8 1 8.4000 -0.2000 67 | g 8 2 -17.6000 -0.1000 68 | h 8 2 -15.3000 0.6000 69 | g 8 3 -0.5000 0.4000 70 | h 8 3 12.8000 -0.2000 71 | g 8 4 -21.1000 -0.1000 72 | h 8 4 -11.7000 0.5000 73 | g 8 5 15.3000 0.4000 74 | h 8 5 14.9000 -0.3000 75 | g 8 6 13.7000 0.3000 76 | h 8 6 3.6000 -0.4000 77 | g 8 7 -16.5000 -0.1000 78 | h 8 7 -6.9000 0.5000 79 | g 8 8 -0.3000 0.4000 80 | h 8 8 2.8000 0 81 | g 9 0 5 0 82 | g 9 1 8.4000 0 83 | h 9 1 -23.4000 0 84 | g 9 2 2.9000 0 85 | h 9 2 11 0 86 | g 9 3 -1.5000 0 87 | h 9 3 9.8000 0 88 | g 9 4 -1.1000 0 89 | h 9 4 -5.1000 0 90 | g 9 5 -13.2000 0 91 | h 9 5 -6.3000 0 92 | g 9 6 1.1000 0 93 | h 9 6 7.8000 0 94 | g 9 7 8.8000 0 95 | h 9 7 0.4000 0 96 | g 9 8 -9.3000 0 97 | h 9 8 -1.4000 0 98 | g 9 9 -11.9000 0 99 | h 9 9 9.6000 0 100 | g 10 0 -1.9000 0 101 | g 10 1 -6.2000 0 102 | h 10 1 3.4000 0 103 | g 10 2 -0.1000 0 104 | h 10 2 -0.2000 0 105 | g 10 3 1.7000 0 106 | h 10 3 3.6000 0 107 | g 10 4 -0.9000 0 108 | h 10 4 4.8000 0 109 | g 10 5 0.7000 0 110 | h 10 5 -8.6000 0 111 | g 10 6 -0.9000 0 112 | h 10 6 -0.1000 0 113 | g 10 7 1.9000 0 114 | h 10 7 -4.3000 0 115 | g 10 8 1.4000 0 116 | h 10 8 -3.4000 0 117 | g 10 9 -2.4000 0 118 | h 10 9 -0.1000 0 119 | g 10 10 -3.8000 0 120 | h 10 10 -8.8000 0 121 | g 11 0 3 0 122 | g 11 1 -1.4000 0 123 | h 11 1 0 0 124 | g 11 2 -2.5000 0 125 | h 11 2 2.5000 0 126 | g 11 3 2.3000 0 127 | h 11 3 -0.6000 0 128 | g 11 4 -0.9000 0 129 | h 11 4 -0.4000 0 130 | g 11 5 0.3000 0 131 | h 11 5 0.6000 0 132 | g 11 6 -0.7000 0 133 | h 11 6 -0.2000 0 134 | g 11 7 -0.1000 0 135 | h 11 7 -1.7000 0 136 | g 11 8 1.4000 0 137 | h 11 8 -1.6000 0 138 | g 11 9 -0.6000 0 139 | h 11 9 -3 0 140 | g 11 10 0.2000 0 141 | h 11 10 -2 0 142 | g 11 11 3.1000 0 143 | h 11 11 -2.6000 0 144 | g 12 0 -2 0 145 | g 12 1 -0.1000 0 146 | h 12 1 -1.2000 0 147 | g 12 2 0.5000 0 148 | h 12 2 0.5000 0 149 | g 12 3 1.3000 0 150 | h 12 3 1.4000 0 151 | g 12 4 -1.2000 0 152 | h 12 4 -1.8000 0 153 | g 12 5 0.7000 0 154 | h 12 5 0.1000 0 155 | g 12 6 0.3000 0 156 | h 12 6 0.8000 0 157 | g 12 7 0.5000 0 158 | h 12 7 -0.2000 0 159 | g 12 8 -0.3000 0 160 | h 12 8 0.6000 0 161 | g 12 9 -0.5000 0 162 | h 12 9 0.2000 0 163 | g 12 10 0.1000 0 164 | h 12 10 -0.9000 0 165 | g 12 11 -1.1000 0 166 | h 12 11 0 0 167 | g 12 12 -0.3000 0 168 | h 12 12 0.5000 0 169 | g 13 0 0.1000 0 170 | g 13 1 -0.9000 0 171 | h 13 1 -0.9000 0 172 | g 13 2 0.5000 0 173 | h 13 2 0.6000 0 174 | g 13 3 0.7000 0 175 | h 13 3 1.4000 0 176 | g 13 4 -0.3000 0 177 | h 13 4 -0.4000 0 178 | g 13 5 0.8000 0 179 | h 13 5 -1.3000 0 180 | g 13 6 0 0 181 | h 13 6 -0.1000 0 182 | g 13 7 0.8000 0 183 | h 13 7 0.3000 0 184 | g 13 8 0 0 185 | h 13 8 -0.1000 0 186 | g 13 9 0.4000 0 187 | h 13 9 0.5000 0 188 | g 13 10 0.1000 0 189 | h 13 10 0.5000 0 190 | g 13 11 0.5000 0 191 | h 13 11 -0.4000 0 192 | g 13 12 -0.5000 0 193 | h 13 12 -0.4000 0 194 | g 13 13 -0.4000 0 195 | h 13 13 -0.6000 0 -------------------------------------------------------------------------------- /igrf/matlab/igrf_coeffs/norm_g_2020.txt: -------------------------------------------------------------------------------- 1 | 1 0 -29405 5.7 2 | 1 1 -1450.9 7.4 3 | 2 0 -3749.4 -16.5 4 | 2 1 5165 -12.124 5 | 2 2 1452.3 -1.8187 6 | 3 0 3408 5.5 7 | 3 1 -7290.9 -18.065 8 | 3 2 2393.9 6.0031 9 | 3 3 415.6 -9.4868 10 | 4 0 3950.6 -5.25 11 | 4 1 4479.8 -8.8544 12 | 4 2 337.7 -23.087 13 | 4 3 -647.16 10.877 14 | 4 4 35.496 -3.7715 15 | 5 0 -1845.1 -2.3625 16 | 5 1 3692.5 5.0833 17 | 5 2 1443.3 -4.6111 18 | 5 3 -662.16 0.94124 19 | 5 4 -335.44 2.8841 20 | 5 5 9.4711 0.6314 21 | 6 0 952.88 -7.2188 22 | 6 1 1238.2 -5.6709 23 | 6 2 1089.4 5.9777 24 | 6 3 -1210.5 12.952 25 | 6 4 -197.54 -7.6396 26 | 6 5 31.412 0 27 | 6 6 -43.459 0.60452 28 | 7 0 2161.1 -2.6813 29 | 7 1 -2720.5 -7.0939 30 | 7 2 -237.48 0 31 | 7 3 1157 14.335 32 | 7 4 195.11 1.2349 33 | 7 5 39.517 -3.0872 34 | 7 6 -17.437 -1.9375 35 | 7 7 6.3431 0.51781 36 | 8 0 1191.5 0 37 | 8 1 650.2 6.7031 38 | 8 2 -987.05 -5.6082 39 | 8 3 -20.71 16.568 40 | 8 4 -564.13 -2.6736 41 | 8 5 226.91 5.9322 42 | 8 6 94.054 2.0596 43 | 8 7 -41.363 -0.25068 44 | 8 8 -0.18801 0.25068 45 | 9 0 474.8 0 46 | 9 1 1070.2 0 47 | 9 2 315.09 0 48 | 9 3 -124.47 0 49 | 9 4 -62.013 0 50 | 9 5 -444.72 0 51 | 9 6 19.138 0 52 | 9 7 66.295 0 53 | 9 8 -24.031 0 54 | 9 9 -7.2477 0 55 | 10 0 -342.81 0 56 | 10 1 -1508.4 0 57 | 10 2 -21.069 0 58 | 10 3 280.98 0 59 | 10 4 -105.18 0 60 | 10 5 51.741 0 61 | 10 6 -37.188 0 62 | 10 7 38.082 0 63 | 10 8 11.456 0 64 | 10 9 -6.3715 0 65 | 10 10 -2.2558 0 66 | 11 0 1033.3 0 67 | 11 1 -652.94 0 68 | 11 2 -1022.6 0 69 | 11 3 754.33 0 70 | 11 4 -215.56 0 71 | 11 5 47.527 0 72 | 11 6 -65.882 0 73 | 11 7 -4.9604 0 74 | 11 8 31.864 0 75 | 11 9 -5.289 0 76 | 11 10 0.54407 0 77 | 11 11 1.7979 0 78 | 12 0 -1320.4 0 79 | 12 1 -89.703 0 80 | 12 2 397.56 0 81 | 12 3 843.99 0 82 | 12 4 -584.3 0 83 | 12 5 233.81 0 84 | 12 6 62.49 0 85 | 12 7 58.527 0 86 | 12 8 -17.558 0 87 | 12 9 -12.772 0 88 | 12 10 0.94325 0 89 | 12 11 -3.0596 0 90 | 12 12 -0.17033 0 91 | 13 0 126.96 0 92 | 13 1 -1557.2 0 93 | 13 2 773.76 0 94 | 13 3 898.19 0 95 | 13 4 -295.24 0 96 | 13 5 556.7 0 97 | 13 6 0 0 98 | 13 7 213.71 0 99 | 13 8 0 0 100 | 13 9 27.229 0 101 | 13 10 2.8388 0 102 | 13 11 5.0184 0 103 | 13 12 -1.4194 0 104 | 13 13 -0.2227 0 105 | -------------------------------------------------------------------------------- /igrf/matlab/main.m: -------------------------------------------------------------------------------- 1 | % Rishav (2021/12/27) 2 | clc 3 | clear 4 | close all 5 | 6 | latitude = 28.3949*0; 7 | longitude = 84.1240*0; 8 | radius = 6371.2; 9 | days = 0; 10 | 11 | [Bn, Be, Bd] = igrf(latitude, longitude, radius, days); -------------------------------------------------------------------------------- /spacetime/Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | CFLAGS = -std=c99 -Wall -Wextra -Werror 3 | 4 | TARGET = tests 5 | BUILD_DIR = build 6 | 7 | INC = \ 8 | -I../attitude \ 9 | -Iiau06 10 | 11 | SRC = \ 12 | ../attitude/dcm.c \ 13 | ../attitude/maps.c \ 14 | iau06/iau06.c \ 15 | iau06/iau06_data.c \ 16 | time.c \ 17 | frame.c \ 18 | test_frame.c 19 | 20 | ifeq ($(OS),Windows_NT) 21 | TARGET := $(TARGET).exe 22 | RM = del /q 23 | MKDIR = mkdir 24 | else 25 | RM = rm -rf 26 | MKDIR = mkdir -p 27 | endif 28 | 29 | all: $(TARGET) 30 | 31 | $(TARGET): $(SRC) 32 | @if not exist $(BUILD_DIR) $(MKDIR) $(BUILD_DIR) 33 | $(CC) $(CFLAGS) $(INC) -o $(BUILD_DIR)/$(TARGET) $(SRC) 34 | 35 | run: all 36 | $(BUILD_DIR)/$(TARGET) 37 | 38 | clean: 39 | $(RM) $(BUILD_DIR) 40 | 41 | .PHONY: all run clean 42 | -------------------------------------------------------------------------------- /spacetime/README.md: -------------------------------------------------------------------------------- 1 | # spacetime 2 | 3 | Useful coordinate transformations and time conversions. 4 | 5 | ## Coordinates 6 | 7 | Index of coordinates used in the software. 8 | 9 | 1. `eci` - Earth Centered Inertial 10 | 2. `ecef` - Earth Centered Earth Fixed 11 | 3. `lvlh` - Local Vertical Local Horizontal 12 | 4. `llh` - Latitude Longitude Height (Geodetic) 13 | 5. `llr` - Latitude Longitude Radius (Geocentric) 14 | 15 | ## Notes 16 | 17 | 1. Computation of Julian day at 0 UT (`j0` in software) holds from 1900 to 2100 [1]. 18 | 19 | ## References 20 | 21 | 1. Curtis - Orbital mechanics for engineering students (2020) 22 | -------------------------------------------------------------------------------- /spacetime/frame.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "attitude.h" 5 | #include "time.h" 6 | #include "iau06.h" 7 | 8 | #define D2R 0.01745329251 9 | #define R2D 57.2957795131 10 | #define PI_2 1.57079632679489661923 11 | #define PI 2 * 1.57079632679489661923 12 | 13 | void print_dcm(const double dcm[3][3]) 14 | { 15 | for (int i = 0; i < 3; i++) 16 | { 17 | for (int j = 0; j < 3; j++) 18 | { 19 | printf(" %.20f ", dcm[i][j]); 20 | } 21 | printf("\n"); 22 | } 23 | } 24 | 25 | void frame_eci_to_ecef_dcm(const utc_t utc, double dcm[3][3]) 26 | { 27 | double W[3][3], R[3][3], Q[3][3]; 28 | 29 | const double jd_since_j2000 = 51544.5; 30 | const double jd = time_julian_date(utc) - 2400000.5f; 31 | const double jd_elapsed = jd - jd_since_j2000; 32 | const double jd_fraction = fmod((fmod(jd_elapsed, 1.0) + 1.0), 1.0); 33 | double t = jd_elapsed / 36525.0; 34 | 35 | double xp = 0.0, yp = 0.0; 36 | const double s_prime = -0.000047 * t * D2R / 3600.0; 37 | const double zyx[3] = {s_prime, xp, yp}; 38 | euler_to_dcm(zyx, EULER_ZYX, W); 39 | 40 | const double era = fmod(2.0 * PI * (jd_fraction + 0.7790572732640 + 0.00273781191135448 * jd_elapsed), 2.0 * PI); 41 | dcm_z(era, R); 42 | 43 | double x, y, s; 44 | iau06_get_xys(t, &x, &y, &s); 45 | 46 | double E = atan2(y, x); 47 | double d = atan(sqrt((x * x + y * y) / (1.0 - x * x - y * y))); 48 | const double zyz[3] = {E, d, -E - s}; 49 | euler_to_dcm(zyz, EULER_ZYZ, Q); 50 | 51 | double WR[3][3]; 52 | dcm_prod(W, R, WR); 53 | dcm_prod(WR, Q, dcm); 54 | } 55 | 56 | // void frame_ecef_to_ned(const double v_ecef[3], const double llr[3], double v_ned[3]) 57 | // { 58 | // const double st = sin(llr[0]); 59 | // const double ct = cos(llr[0]); 60 | // const double sp = sin(llr[1]); 61 | // const double cp = cos(llr[1]); 62 | // const double t = cp * v_ned[0] + sp * v_ned[1]; 63 | 64 | // v_ned[0] = -st * t + ct * v_ned[2]; 65 | // v_ned[1] = -sp * v_ned[0] + cp * v_ned[1]; 66 | // v_ned[2] = -ct * t - st * v_ned[2]; 67 | // } 68 | 69 | // void frame_ned_to_ecef(const double v_ned[3], const double llr[3], double v_ecef[3]) 70 | // { 71 | // const double st = sin(llr[0]); 72 | // const double ct = cos(llr[0]); 73 | // const double sp = sin(llr[1]); 74 | // const double cp = cos(llr[1]); 75 | // const double t = - ct * v_ned[2] - st * v_ned[0]; 76 | 77 | // v_ecef[0] = cp * t - sp * v_ned[1]; 78 | // v_ecef[1] = sp * t + cp * v_ned[1]; 79 | // v_ecef[2] = - st * v_ned[2] - ct * v_ned[0]; 80 | // } 81 | -------------------------------------------------------------------------------- /spacetime/frame.h: -------------------------------------------------------------------------------- 1 | // rms (2025-02-03) 2 | 3 | #ifndef _ADCS_FRAME_H_ 4 | #define _ADCS_FRAME_H_ 5 | 6 | #include "frame.h" 7 | #include "time.h" 8 | 9 | #ifdef __cplusplus 10 | extern "C" 11 | { 12 | #endif 13 | 14 | void frame_eci_to_ecef_dcm(const utc_t t, double dcm[3][3]); 15 | 16 | void frame_ecef_to_ned(const double v_ecef[3], const double llr[3], double v_ned[3]); 17 | void frame_ned_to_ecef(const double v_ned[3], const double llr[3], double v_ecef[3]); 18 | 19 | // void frame_eci_to_ned(); 20 | // void frame_ned_to_eci(); 21 | 22 | // void frame_ned_to_ecef_dcm(const double llr[3]); 23 | // void frame_ecef_to_ned_dcm(const double llr[3]); 24 | 25 | #ifdef __cplusplus 26 | } 27 | #endif 28 | 29 | #endif // frame.h 30 | -------------------------------------------------------------------------------- /spacetime/iau06/iau06.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "iau06.h" 4 | #include "iau06_data.h" 5 | 6 | #define PI 2 * 1.57079632679489661923 7 | #define D2R 0.01745329251 8 | 9 | void iau06_compute_nutation_v(const double t[5], double nv[14]) 10 | { 11 | double m_moon = 485868.249036 + 1717915923.2178 * t[0] + 31.8792 * t[1] + 0.051635 * t[2] - 0.00024470 * t[3]; 12 | double m_sun = 1287104.793048 + 129596581.0481 * t[0] - 0.5532 * t[1] + 0.000136 * t[2] - 0.00001149 * t[3]; 13 | double mu_moon = 335779.526232 + 1739527262.8478 * t[0] - 12.7512 * t[1] - 0.001037 * t[2] + 0.00000417 * t[3]; 14 | double d_sun = 1072260.703692 + 1602961601.2090 * t[0] - 6.3706 * t[1] + 0.006593 * t[2] - 0.00003169 * t[3]; 15 | double w_moon = 450160.398036 - 6962890.5431 * t[0] + 7.4722 * t[1] + 0.007702 * t[2] - 0.00005939 * t[3]; 16 | 17 | double l_mercury = 4.402608842 + 2608.7903141574 * t[0]; 18 | double l_venus = 3.176146697 + 1021.3285546211 * t[0]; 19 | double l_earth = 1.753470314 + 628.3075849991 * t[0]; 20 | double l_mars = 6.203480913 + 334.06124267 * t[0]; 21 | double l_jupiter = 0.599546497 + 52.9690962641 * t[0]; 22 | double l_saturn = 0.874016757 + 21.329910496 * t[0]; 23 | double l_uranus = 5.481293872 + 7.4781598567 * t[0]; 24 | double l_neptune = 5.311886287 + 3.8133035638 * t[0]; 25 | 26 | double pa = 0.02438175 * t[0] + 0.00000538691 * t[1]; 27 | 28 | double arcsec_to_rad = D2R / 3600.0; 29 | nv[0] = fmod(m_moon * arcsec_to_rad, 2 * PI); 30 | nv[1] = fmod(m_sun * arcsec_to_rad, 2 * PI); 31 | nv[2] = fmod(mu_moon * arcsec_to_rad, 2 * PI); 32 | nv[3] = fmod(d_sun * arcsec_to_rad, 2 * PI); 33 | nv[4] = fmod(w_moon * arcsec_to_rad, 2 * PI); 34 | nv[5] = fmod(l_mercury, 2 * PI); 35 | nv[6] = fmod(l_venus, 2 * PI); 36 | nv[7] = fmod(l_earth, 2 * PI); 37 | nv[8] = fmod(l_mars, 2 * PI); 38 | nv[9] = fmod(l_jupiter, 2 * PI); 39 | nv[10] = fmod(l_saturn, 2 * PI); 40 | nv[11] = fmod(l_uranus, 2 * PI); 41 | nv[12] = fmod(l_neptune, 2 * PI); 42 | nv[13] = fmod(pa, 2 * PI); 43 | } 44 | 45 | double iau06_compute_x(const double nv[14], const double t[5]) 46 | { 47 | double q; 48 | double x = 0.0; 49 | 50 | for (int i = 0; i < 1306; i++) 51 | { 52 | q = 0.0; 53 | for (int k = 0; k < 14; k++) 54 | { 55 | q += iau06_cip_x0[i][k + 3] * nv[k]; 56 | } 57 | x += (iau06_cip_x0[i][1] * sin(q) + iau06_cip_x0[i][2] * cos(q)); 58 | } 59 | 60 | for (int i = 0; i < 253; i++) 61 | { 62 | q = 0.0; 63 | for (int k = 0; k < 14; k++) 64 | { 65 | q += iau06_cip_x1[i][k + 3] * nv[k]; 66 | } 67 | x += (iau06_cip_x1[i][1] * sin(q) + iau06_cip_x1[i][2] * cos(q)) * t[0]; 68 | } 69 | 70 | for (int i = 0; i < 36; i++) 71 | { 72 | q = 0.0; 73 | for (int k = 0; k < 14; k++) 74 | { 75 | q += iau06_cip_x2[i][k + 3] * nv[k]; 76 | } 77 | x += (iau06_cip_x2[i][1] * sin(q) + iau06_cip_x2[i][2] * cos(q)) * t[1]; 78 | } 79 | 80 | for (int i = 0; i < 4; i++) 81 | { 82 | q = 0.0; 83 | for (int k = 0; k < 14; k++) 84 | { 85 | q += iau06_cip_x3[i][k + 3] * nv[k]; 86 | } 87 | x += (iau06_cip_x3[i][1] * sin(q) + iau06_cip_x3[i][2] * cos(q)) * t[2]; 88 | } 89 | 90 | q = 0.0; 91 | for (int k = 0; k < 14; k++) 92 | { 93 | q += iau06_cip_x4[k + 3] * nv[k]; 94 | } 95 | x += (iau06_cip_x4[1] * sin(q) + iau06_cip_x4[2] * cos(q)) * t[3]; 96 | 97 | return x; 98 | } 99 | 100 | double iau06_compute_y(const double nv[14], const double t[5]) 101 | { 102 | double q; 103 | double y = 0.0; 104 | 105 | for (int i = 0; i < 962; i++) 106 | { 107 | q = 0.0; 108 | for (int k = 0; k < 14; k++) 109 | { 110 | q += iau06_cip_y0[i][k + 3] * nv[k]; 111 | } 112 | y += (iau06_cip_y0[i][1] * sin(q) + iau06_cip_y0[i][2] * cos(q)); 113 | } 114 | 115 | for (int i = 0; i < 277; i++) 116 | { 117 | q = 0.0; 118 | for (int k = 0; k < 14; k++) 119 | { 120 | q += iau06_cip_y1[i][k + 3] * nv[k]; 121 | } 122 | y += (iau06_cip_y1[i][1] * sin(q) + iau06_cip_y1[i][2] * cos(q)) * t[0]; 123 | } 124 | 125 | for (int i = 0; i < 30; i++) 126 | { 127 | q = 0.0; 128 | for (int k = 0; k < 14; k++) 129 | { 130 | q += iau06_cip_y2[i][k + 3] * nv[k]; 131 | } 132 | y += (iau06_cip_y2[i][1] * sin(q) + iau06_cip_y2[i][2] * cos(q)) * t[1]; 133 | } 134 | 135 | for (int i = 0; i < 5; i++) 136 | { 137 | q = 0.0; 138 | for (int k = 0; k < 14; k++) 139 | { 140 | q += iau06_cip_y3[i][k + 3] * nv[k]; 141 | } 142 | y += (iau06_cip_y3[i][1] * sin(q) + iau06_cip_y3[i][2] * cos(q)) * t[2]; 143 | } 144 | 145 | q = 0.0; 146 | for (int k = 0; k < 14; k++) 147 | { 148 | q += iau06_cip_y4[k + 3] * nv[k]; 149 | } 150 | y += (iau06_cip_y4[1] * sin(q) + iau06_cip_y4[2] * cos(q)) * t[3]; 151 | 152 | return y; 153 | } 154 | 155 | double iau06_compute_s(const double nv[14], const double t[5]) 156 | { 157 | double q; 158 | double s = 0.0; 159 | 160 | for (int i = 0; i < 33; i++) 161 | { 162 | q = 0.0; 163 | for (int k = 0; k < 8; k++) 164 | { 165 | int nut_index = (k < 5) ? k : (k + 2); 166 | q += iau06_cip_s0[i][k + 3] * nv[nut_index]; 167 | } 168 | s += (iau06_cip_s0[i][1] * sin(q) + iau06_cip_s0[i][2] * cos(q)); 169 | } 170 | 171 | for (int i = 0; i < 3; i++) 172 | { 173 | q = 0.0; 174 | for (int k = 0; k < 8; k++) 175 | { 176 | int nut_index = (k < 5) ? k : (k + 2); 177 | q += iau06_cip_s1[i][k + 3] * nv[nut_index]; 178 | } 179 | s += (iau06_cip_s1[i][1] * sin(q) + iau06_cip_s1[i][2] * cos(q)) * t[0]; 180 | } 181 | 182 | for (int i = 0; i < 25; i++) 183 | { 184 | q = 0.0; 185 | for (int k = 0; k < 8; k++) 186 | { 187 | int nut_index = (k < 5) ? k : (k + 2); 188 | q += iau06_cip_s2[i][k + 3] * nv[nut_index]; 189 | } 190 | s += (iau06_cip_s2[i][1] * sin(q) + iau06_cip_s2[i][2] * cos(q)) * t[1]; 191 | } 192 | 193 | for (int i = 0; i < 4; i++) 194 | { 195 | q = 0.0; 196 | for (int k = 0; k < 8; k++) 197 | { 198 | int nut_index = (k < 5) ? k : (k + 2); 199 | q += iau06_cip_s3[i][k + 3] * nv[nut_index]; 200 | } 201 | s += (iau06_cip_s3[i][1] * sin(q) + iau06_cip_s3[i][2] * cos(q)) * t[2]; 202 | } 203 | 204 | q = 0.0; 205 | for (int k = 0; k < 8; k++) 206 | { 207 | int nut_index = (k < 5) ? k : (k + 2); 208 | q += iau06_cip_s4[k + 3] * nv[nut_index]; 209 | } 210 | s += (iau06_cip_s4[1] * sin(q) + iau06_cip_s4[2] * cos(q)) * t[3]; 211 | 212 | return s; 213 | } 214 | 215 | void iau06_get_xys(const double tt, double *x, double *y, double *s) 216 | { 217 | double t[5] = {tt, pow(tt, 2.0), pow(tt, 3.0), pow(tt, 4.0), pow(tt, 5.0)}; 218 | double nv[14]; 219 | iau06_compute_nutation_v(t, nv); 220 | 221 | double x0 = -16617.0 + 2004191898.0 * t[0] - 429782.9 * t[1] - 198618.34 * t[2] + 7.578 * t[3] + 5.9285 * t[4]; 222 | double y0 = -6951.0 - 25896.0 * t[0] - 22407274.7 * t[1] + 1900.59 * t[2] + 1112.526 * t[3] + 0.1358 * t[4]; 223 | double s0 = 94.0 + 3808.65 * t[0] - 122.68 * t[1] - 72574.11 * t[2] + 27.98 * t[3] + 15.62 * t[4]; 224 | 225 | double x_temp = x0 + iau06_compute_x(nv, t); 226 | double y_temp = y0 + iau06_compute_y(nv, t); 227 | double s_temp = s0 + iau06_compute_s(nv, t); 228 | 229 | *x = (x_temp * 1e-6 / 3600.0) * D2R; 230 | *y = (y_temp * 1e-6 / 3600.0) * D2R; 231 | *s = (s_temp * 1e-6 / 3600.0) * D2R - (*x) * (*y) / 2.0; 232 | } 233 | -------------------------------------------------------------------------------- /spacetime/iau06/iau06.h: -------------------------------------------------------------------------------- 1 | #ifndef _IAU06_H_ 2 | #define _IAU06_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" 6 | { 7 | #endif 8 | 9 | void iau06_get_xys(const double t, double *x, double *y, double *s); 10 | 11 | #ifdef __cplusplus 12 | } 13 | #endif 14 | 15 | #endif // iau06_.h 16 | -------------------------------------------------------------------------------- /spacetime/iau06/iau06_data.h: -------------------------------------------------------------------------------- 1 | #ifndef _IAU06_DATA_H_ 2 | #define _IAU06_DATA_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" 6 | { 7 | #endif 8 | 9 | extern const double iau06_cip_s0[33][17]; 10 | extern const double iau06_cip_s1[3][17]; 11 | extern const double iau06_cip_s2[25][17]; 12 | extern const double iau06_cip_s3[4][17]; 13 | extern const double iau06_cip_s4[17]; 14 | 15 | extern const double iau06_cip_x0[1306][17]; 16 | extern const double iau06_cip_x1[253][17]; 17 | extern const double iau06_cip_x2[36][17]; 18 | extern const double iau06_cip_x3[4][17]; 19 | extern const double iau06_cip_x4[17]; 20 | 21 | extern const double iau06_cip_y0[962][17]; 22 | extern const double iau06_cip_y1[277][17]; 23 | extern const double iau06_cip_y2[30][17]; 24 | extern const double iau06_cip_y3[5][17]; 25 | extern const double iau06_cip_y4[17]; 26 | 27 | #ifdef __cplusplus 28 | } 29 | #endif 30 | 31 | #endif // iau06_data.h 32 | -------------------------------------------------------------------------------- /spacetime/test_frame.c: -------------------------------------------------------------------------------- 1 | // rms (2025-02-27 00:58:43) 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "time.h" 9 | #include "frame.h" 10 | #include "attitude.h" 11 | 12 | #define RED "\x1b[31m" 13 | #define GREEN "\x1b[32m" 14 | #define RESET "\x1b[0m" 15 | 16 | // Error tolerance 17 | #define TOLERANCE 1e-8 18 | 19 | // Is the deviation within threshold? 20 | bool compare_matrices(double mat1[3][3], double mat2[3][3]) 21 | { 22 | for (int i = 0; i < 3; i++) 23 | { 24 | for (int j = 0; j < 3; j++) 25 | { 26 | if (fabs(mat1[i][j] - mat2[i][j]) > TOLERANCE) 27 | { 28 | return false; 29 | } 30 | } 31 | } 32 | 33 | return true; 34 | } 35 | 36 | bool test_single_case(utc_t t, double ground_truth[3][3], const char *test_name) 37 | { 38 | double dcm[3][3]; 39 | frame_eci_to_ecef_dcm(t, dcm); 40 | 41 | bool status = compare_matrices(dcm, ground_truth); 42 | 43 | if (status) 44 | { 45 | printf(GREEN "Test %s PASSED!\n", test_name); 46 | } 47 | else 48 | { 49 | printf(RED "Test %s FAILED.\n", test_name); 50 | } 51 | 52 | return status; 53 | } 54 | 55 | bool test_frame_eci_to_ecef_dcm() 56 | { 57 | bool status = true; 58 | 59 | // Test cases 60 | utc_t test_times[] = 61 | { 62 | {.year = 2024, .month = 1, .day = 12, .hour = 4, .minute = 52, .second = 12}, 63 | {.year = 2023, .month = 6, .day = 15, .hour = 12, .minute = 0, .second = 0}, 64 | {.year = 2022, .month = 12, .day = 31, .hour = 23, .minute = 59, .second = 59}, 65 | {.year = 2021, .month = 3, .day = 20, .hour = 12, .minute = 0, .second = 0}, 66 | {.year = 2020, .month = 7, .day = 4, .hour = 18, .minute = 30, .second = 0}, 67 | {.year = 2019, .month = 9, .day = 23, .hour = 6, .minute = 0, .second = 0}, 68 | {.year = 2018, .month = 2, .day = 14, .hour = 10, .minute = 15, .second = 30}, 69 | {.year = 2017, .month = 5, .day = 1, .hour = 0, .minute = 0, .second = 0}, 70 | {.year = 2016, .month = 8, .day = 8, .hour = 8, .minute = 8, .second = 8}, 71 | {.year = 2015, .month = 11, .day = 11, .hour = 11, .minute = 11, .second = 11}, 72 | {.year = 2014, .month = 4, .day = 22, .hour = 16, .minute = 45, .second = 0}, 73 | {.year = 2013, .month = 10, .day = 31, .hour = 20, .minute = 0, .second = 0} 74 | }; 75 | 76 | // Ground truth DCMs from MATLAB for each test case 77 | double ground_truths[][3][3] = { 78 | { // Test Case 1: 2024-01-12 04:52:12 79 | {-0.997637610071718, -0.068657153281647, 0.002322557566614}, 80 | {0.068657043997502, -0.997640313048689, -0.000126844940965}, 81 | {0.002325785870385, 0.000032914653284, 0.999997294814696} 82 | }, 83 | { // Test Case 2: 2023-06-15 12:00:00 84 | {0.118135345851863, 0.992997458396542, -0.000296105502455}, 85 | {-0.992994928395361, 0.118135713335329, 0.002241743929254}, 86 | {0.002261026658881, 0.000029202067814, 0.999997443449576} 87 | }, 88 | { // Test Case 3: 2022-12-31 23:59:59 89 | {-0.175236910751489, 0.984526228084138, 0.000362387491732}, 90 | {-0.984523804613696, -0.175237283521158, 0.002184631112492}, 91 | {0.002214330428570, 0.000026048895180, 0.999997548028098} 92 | }, 93 | { // Test Case 4: 2021-03-20 12:00:00 94 | {0.999370507366738, -0.035418595751463, -0.002027826556472}, 95 | {0.035418503094663, 0.999372564652670, -0.000081597043715}, 96 | {0.002029444279118, 0.000009723097811, 0.999997940628569} 97 | }, 98 | { // Test Case 5: 2020-07-04 18:30:00 99 | {-0.936892730101000, -0.349612136140604, 0.001834815276248}, 100 | {0.349611453711205, -0.936894526737318, -0.000690799242776}, 101 | {0.001960540188802, -0.000005732352496, 0.999998078122807} 102 | }, 103 | { // Test Case 6: 2019-09-23 06:00:00 104 | {-0.027062346343195, 0.999633745658313, 0.000062861216759}, 105 | {-0.999631972868165, -0.027062416739603, 0.001882662984409}, 106 | {0.001883674627362, -0.000011888804394, 0.999998225812703} 107 | }, 108 | { // Test Case 7: 2018-02-14 10:15:30 109 | {0.469961212326295, -0.882686660628696, -0.000847382416903}, 110 | {0.882685353543536, 0.469961975056226, -0.001519421262892}, 111 | {0.001739410394906, -0.000033902989508, 0.999998486649888} 112 | }, 113 | { // Test Case 8: 2017-05-01 00:00:00 114 | {-0.778312296779783, -0.627876069100959, 0.001269067258962}, 115 | {0.627875145428585, -0.778313330273630, -0.001077808612440}, 116 | {0.001664462199486, -0.000042055906858, 0.999998613897483} 117 | }, 118 | { // Test Case 9: 2016-08-08 08:08:08 119 | {0.189298101569868, 0.981919630977741, -0.000258925822983}, 120 | {-0.981918352376658, 0.189298273100650, 0.001585265599304}, 121 | {0.001605617623422, -0.000045843750941, 0.999998709944367} 122 | }, 123 | { // Test Case 10: 2015-11-11 11:11:11 124 | {-0.789350900983387, -0.613941165684193, 0.001183298376960}, 125 | {0.613940383190589, -0.789351786440114, -0.000981392680737}, 126 | {0.001536556054151, -0.000048188537779, 0.999998818335980} 127 | }, 128 | { // Test Case 11: 2014-04-22 16:45:00 129 | {-0.203297890121490, 0.979116878640318, 0.000325321059835}, 130 | {-0.979115925705476, -0.203298146341569, 0.001366646988283}, 131 | {0.001404244301801, -0.000040690581393, 0.999999013220622} 132 | }, 133 | { // Test Case 12: 2013-10-31 20:00:00 134 | {0.940301701551049, -0.340339594015884, -0.001292596342967}, 135 | {0.340339325798953, 0.940302589276643, -0.000428852370439}, 136 | {0.001361387129830, -0.000036670754258, 0.999999072639739} 137 | } 138 | }; 139 | 140 | // Input for each test case 141 | const char *test_names[] = 142 | { 143 | "2024-01-12 04:52:12", 144 | "2023-06-15 12:00:00", 145 | "2022-12-31 23:59:59", 146 | "2021-03-20 12:00:00", 147 | "2020-07-04 18:30:00", 148 | "2019-09-23 06:00:00", 149 | "2018-02-14 10:15:30", 150 | "2017-05-01 00:00:00", 151 | "2016-08-08 08:08:08", 152 | "2015-11-11 11:11:11", 153 | "2014-04-22 16:45:00", 154 | "2013-10-31 20:00:00" 155 | }; 156 | 157 | for (size_t i = 0; i < sizeof(test_times) / sizeof(*test_times); i++) 158 | { 159 | status &= test_single_case(test_times[i], ground_truths[i], test_names[i]); 160 | } 161 | 162 | return status; 163 | } 164 | 165 | int main() 166 | { 167 | test_frame_eci_to_ecef_dcm(); 168 | printf(RESET); 169 | 170 | return 0; 171 | } 172 | -------------------------------------------------------------------------------- /spacetime/tests.c: -------------------------------------------------------------------------------- 1 | // Numerical validations 2 | // 2024-12-25 3 | 4 | #include 5 | #include "time.h" 6 | 7 | int main(void) 8 | { 9 | // Example 5.04. Ref [1]. 10 | utc_t utc; 11 | utc.year = 2004; 12 | utc.month = 5; 13 | utc.day = 12; 14 | utc.hour = 14; 15 | utc.minute = 45; 16 | utc.second = 30; 17 | 18 | printf("Julian day number: %f\n", time_julian_date(utc)); // 2453138.115 19 | 20 | // Example 5.06. Ref [1]. 21 | utc.year = 2004; 22 | utc.month = 3; 23 | utc.day = 3; 24 | utc.hour = 4; 25 | utc.minute = 30; 26 | utc.second = 0; 27 | 28 | const double el = 139.0 + 47.0 / 60.0 + 0.0 / 3600.0; 29 | 30 | printf("Local sidereal time [deg]: %f\n", time_local_sidereal_deg(utc, el)); // 8.57688 31 | printf("Local sidereal time [hr]: %f\n", time_local_sidereal_hr(utc, el)); // 0.571792 32 | 33 | return 0; 34 | } 35 | -------------------------------------------------------------------------------- /spacetime/time.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "time.h" 3 | 4 | double fix(double x) 5 | { 6 | if (x > 0) 7 | { 8 | return floor(x); 9 | } 10 | 11 | return ceil(x); 12 | } 13 | 14 | /** 15 | * @brief UTC to Julian date. 16 | * 17 | * Number of decimal days since noon on November 24, 4714 BCE in the proleptic 18 | * Gregorian calendar, or January 1, 4713 BCE in the proleptic Julian calendar. 19 | * 20 | * @param t UTC 21 | * @return Julian date 22 | */ 23 | double time_julian_date(const utc_t t) 24 | { 25 | double j0 = 367 * t.year - fix(7.0 * (t.year + fix((t.month + 9.0) / 12.0)) / 4.0) + fix(275.0 * t.month / 9.0) + t.day + 1721013.5; 26 | double ut = t.hour + t.minute / 60.0 + t.second / 3600.0; 27 | 28 | return j0 + ut / 24.0; 29 | } 30 | 31 | double normalize_zero_to_360(double angle) 32 | { 33 | while (angle < 0) 34 | { 35 | angle += 360.0; 36 | } 37 | 38 | while (angle >= 360.0) 39 | { 40 | angle -= 360.0; 41 | } 42 | 43 | return angle; 44 | } 45 | 46 | // UTC to Greenwich sidereal time. 47 | double time_greenwich_sidereal(const utc_t t) 48 | { 49 | const double j0 = 367 * t.year - fix(7.0 * (t.year + fix((t.month + 9.0) / 12.0)) / 4.0) + fix(275.0 * t.month / 9.0) + t.day + 1721013.5; 50 | const double j = (j0 - 2451545.0) / 36525.0; 51 | const double j_sq = j * j; 52 | const double g0 = normalize_zero_to_360(100.4606184 + 36000.77004 * j + 0.000387933 * j_sq - 2.583e-8 * j_sq * j); 53 | const double ut = t.hour + t.minute / 60.0 + t.second / 3600.0; 54 | 55 | return g0 + 360.98564724 * ut / 24.0; 56 | } 57 | 58 | /** 59 | * @brief UTC to local sidereal time [deg]. 60 | * @param t Time in UTC 61 | * @param el East longitude [deg] 62 | * @return Local sidereal time [deg] 63 | */ 64 | double time_local_sidereal_deg(const utc_t t, const double elon) 65 | { 66 | const double lst = time_greenwich_sidereal(t) + elon; 67 | return lst - 360.0 * fix(lst / 360.0); 68 | } 69 | 70 | // UTC to local sidereal time [hr]. 71 | double time_local_sidereal_hr(const utc_t t, const double elon) 72 | { 73 | return time_local_sidereal_deg(t, elon) / 15.0; 74 | } 75 | -------------------------------------------------------------------------------- /spacetime/time.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @brief Standard time conversions 3 | * @date 2024-12-24 4 | * @cite [1] Curtis - Orbital mechanics for engineering students (2020) 5 | */ 6 | 7 | #ifndef _ADCS_TIME_H_ 8 | #define _ADCS_TIME_H_ 9 | 10 | #include 11 | 12 | #ifdef __cplusplus 13 | extern "C" 14 | { 15 | #endif 16 | 17 | typedef struct 18 | { 19 | uint16_t year; 20 | uint8_t month; 21 | uint8_t day; 22 | uint8_t hour; 23 | uint8_t minute; 24 | uint8_t second; 25 | }utc_t; 26 | 27 | double time_julian_date(const utc_t t); 28 | double time_greenwich_sidereal(const utc_t t); 29 | double time_local_sidereal_hr(const utc_t t, const double elon); 30 | double time_local_sidereal_deg(const utc_t t, const double elon); 31 | 32 | #ifdef __cplusplus 33 | } 34 | #endif 35 | 36 | #endif // time.h 37 | --------------------------------------------------------------------------------