1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
// Copyright (c) 2021 Marco Boneberger
// Licensed under the EUPL-1.2-or-later

//! Contains the franka::RobotState types.
use std::time::Duration;

use crate::robot::errors::{FrankaErrorKind, FrankaErrors};
use crate::robot::types::{RobotMode, RobotStateIntern};
use nalgebra::{Matrix3, Vector3};

/// Describes the robot state.
#[derive(Debug, Clone, Default)]
#[allow(non_snake_case)]
pub struct RobotState {
    /// ![^{O}T_{EE}](https://latex.codecogs.com/png.latex?^{O}T_{EE})
    ///
    /// Measured end effector pose in base frame.
    /// Pose is represented as a 4x4 matrix in column-major format.
    pub O_T_EE: [f64; 16],
    /// ![{^OT_{EE}}_{d}](http://latex.codecogs.com/png.latex?{^OT_{EE}}_{d})
    ///
    /// Last desired end effector pose of motion generation in base frame.
    /// Pose is represented as a 4x4 matrix in column-major format.
    pub O_T_EE_d: [f64; 16],
    /// ![^{F}T_{EE}](http://latex.codecogs.com/png.latex?^{F}T_{EE})
    ///
    /// End effector frame pose in flange frame.
    /// Pose is represented as a 4x4 matrix in column-major format.
    /// # See
    /// * [`F_T_NE`](`Self::F_T_NE`)
    /// * [`NE_T_EE`](`Self::NE_T_EE`)
    /// * [`Robot`](`crate::Robot`) for an explanation of the NE and EE frames.
    pub F_T_EE: [f64; 16],
    /// ![^{F}T_{NE}](https://latex.codecogs.com/png.latex?^{F}T_{NE})
    ///
    /// Nominal end effector frame pose in flange frame.
    /// Pose is represented as a 4x4 matrix in column-major format.
    /// # See
    /// * [`F_T_NE`](`Self::F_T_NE`)
    /// * [`NE_T_EE`](`Self::NE_T_EE`)
    /// * [`Robot`](`crate::Robot`) for an explanation of the NE and EE frames.
    pub F_T_NE: [f64; 16],
    /// ![^{NE}T_{EE}](https://latex.codecogs.com/png.latex?^{NE}T_{EE})
    ///
    /// End effector frame pose in nominal end effector frame.
    /// Pose is represented as a 4x4 matrix in column-major format.
    /// # See
    /// * [`F_T_NE`](`Self::F_T_NE`)
    /// * [`NE_T_EE`](`Self::NE_T_EE`)
    /// * [`Robot`](`crate::Robot`) for an explanation of the NE and EE frames.
    pub NE_T_EE: [f64; 16],
    /// ![^{EE}T_{K}](https://latex.codecogs.com/png.latex?^{EE}T_{K})
    ///
    /// Stiffness frame pose in end effector frame.
    /// Pose is represented as a 4x4 matrix in column-major format.
    ///
    /// See also [K-Frame](`crate::Robot#stiffness-frame-k`)
    pub EE_T_K: [f64; 16],
    /// ![m_{EE}](https://latex.codecogs.com/png.latex?m_{EE})
    ///
    /// Configured mass of the end effector.
    pub m_ee: f64,
    /// ![I_{EE}](https://latex.codecogs.com/png.latex?I_{EE})
    ///
    /// Configured rotational inertia matrix of the end effector load with respect to center of mass.
    pub I_ee: [f64; 9],
    /// ![^{F}x_{C_{EE}}](https://latex.codecogs.com/png.latex?^{F}x_{C_{EE}})
    ///
    /// Configured center of mass of the end effector load with respect to flange frame.
    pub F_x_Cee: [f64; 3],
    /// ![m_{load}](https://latex.codecogs.com/png.latex?m_{load})
    ///
    /// Configured mass of the external load.
    pub m_load: f64,
    /// ![I_{load}](https://latex.codecogs.com/png.latex?I_{load})
    ///
    /// Configured rotational inertia matrix of the external load with respect to center of mass.
    pub I_load: [f64; 9],
    /// ![^{F}x_{C_{load}}](https://latex.codecogs.com/png.latex?^{F}x_{C_{load}})
    ///
    /// Configured center of mass of the external load with respect to flange frame.
    pub F_x_Cload: [f64; 3],
    /// ![m_{total}](https://latex.codecogs.com/png.latex?m_{total})
    ///
    /// Sum of the mass of the end effector and the external load.
    pub m_total: f64,
    /// ![I_{total}](https://latex.codecogs.com/png.latex?I_{total})
    ///
    /// Combined rotational inertia matrix of the end effector load and the external load with respect
    /// to the center of mass.
    pub I_total: [f64; 9],
    /// ![^{F}x_{C_{total}}](https://latex.codecogs.com/png.latex?^{F}x_{C_{total}})
    ///
    /// Combined center of mass of the end effector load and the external load with respect to flange
    /// frame.
    pub F_x_Ctotal: [f64; 3],
    /// Elbow configuration.
    ///
    /// The values of the array are:
    ///  - \[0\] Position of the 3rd joint in \[rad\].
    ///  - \[1\] Sign of the 4th joint. Can be +1 or -1.
    pub elbow: [f64; 2],
    /// Desired elbow configuration.
    ///
    /// The values of the array are:
    ///  - \[0\] Position of the 3rd joint in \[rad\].
    ///  - \[1\] Sign of the 4th joint. Can be +1 or -1.
    pub elbow_d: [f64; 2],
    /// Commanded elbow configuration.
    ///
    /// The values of the array are:
    ///  - \[0\] Position of the 3rd joint in \[rad\].
    ///  - \[1\] Sign of the 4th joint. Can be +1 or -1.
    pub elbow_c: [f64; 2],
    /// Commanded elbow velocity.
    ///
    /// The values of the array are:
    ///  - \[0\] Velocity of the 3rd joint in \[rad/s\].
    ///  - \[1\] Sign of the 4th joint. Can be +1 or -1.
    pub delbow_c: [f64; 2],
    /// Commanded elbow acceleration.
    ///
    /// The values of the array are:
    ///  - \[0\] Acceleration of the 3rd joint in \[rad/s^2\].
    ///  - \[1\] Sign of the 4th joint. Can be +1 or -1.
    pub ddelbow_c: [f64; 2],
    /// ![\tau_{J}](https://latex.codecogs.com/png.latex?\tau_{J})
    ///
    /// Measured link-side joint torque sensor signals. Unit: \[Nm\]
    pub tau_J: [f64; 7],
    /// ![{\tau_J}_d](https://latex.codecogs.com/png.latex?{\tau_J}_d)
    ///
    /// Desired link-side joint torque sensor signals without gravity. Unit:  \[Nm\]
    pub tau_J_d: [f64; 7],
    /// ![\dot{\tau_{J}}](https://latex.codecogs.com/png.latex?\dot{\tau_{J}})
    ///
    /// Derivative of measured link-side joint torque sensor signals. Unit: [Nm/s]
    pub dtau_J: [f64; 7],
    /// ![q](https://latex.codecogs.com/png.latex?q)
    ///
    /// Measured joint position. Unit: \[rad\]
    pub q: [f64; 7],
    /// ![q_d](https://latex.codecogs.com/png.latex?q_d)
    ///
    /// Desired joint position. Unit: \[rad\]
    pub q_d: [f64; 7],
    /// ![\dot{q}](https://latex.codecogs.com/png.latex?\dot{q})
    ///
    /// Measured joint velocity. Unit: \[rad/s\]
    pub dq: [f64; 7],
    /// ![\dot{q}_d](https://latex.codecogs.com/png.latex?\dot{q}_d)
    ///
    /// Desired joint velocity. Unit: \[rad/s\]
    pub dq_d: [f64; 7],
    /// ![\ddot{q}_d](https://latex.codecogs.com/png.latex?\ddot{q}_d)
    ///
    /// Desired joint acceleration. Unit: \[rad/s^2\]
    pub ddq_d: [f64; 7],
    /// Indicates which contact level is activated in which joint. After contact disappears, value
    /// turns to zero.
    ///
    /// See [`Robot::set_collision_behavior`](`crate::Robot::set_collision_behavior`)
    /// for setting sensitivity values.
    pub joint_contact: [f64; 7],
    /// Indicates which contact level is activated in which Cartesian dimension (x,y,z,R,P,Y).
    /// After contact disappears, the value turns to zero.
    ///
    /// See [`Robot::set_collision_behavior`](`crate::Robot::set_collision_behavior`)
    /// for setting sensitivity values.
    pub cartesian_contact: [f64; 6],
    /// Indicates which contact level is activated in which joint. After contact disappears, the value
    /// stays the same until a reset command is sent.
    ///
    /// See [`Robot::set_collision_behavior`](`crate::Robot::set_collision_behavior`)
    /// for setting sensitivity values.
    ///
    /// See [`Robot::automatic_error_recovery`](`crate::Robot::automatic_error_recovery`)
    /// for performing a reset after a collision.
    pub joint_collision: [f64; 7],
    /// Indicates which contact level is activated in which Cartesian dimension (x,y,z,R,P,Y).
    /// After contact disappears, the value stays the same until a reset command is sent.
    ///
    /// See [`Robot::set_collision_behavior`](`crate::Robot::set_collision_behavior`)
    /// for setting sensitivity values.
    ///
    /// See [`Robot::automatic_error_recovery`](`crate::Robot::automatic_error_recovery`)
    /// for performing a reset after a collision.
    pub cartesian_collision: [f64; 6],
    /// ![\hat{\tau}_{\text{ext}}](https://latex.codecogs.com/png.latex?\hat{\tau}_{\text{ext}})
    ///
    /// External torque, filtered. Unit: \[Nm\]
    pub tau_ext_hat_filtered: [f64; 7],
    /// ![^OF_{K,\text{ext}}](https://latex.codecogs.com/png.latex?^OF_{K,\text{ext}})
    ///
    /// Estimated external wrench (force, torque) acting on stiffness frame, expressed
    /// relative to the base frame. See also @ref k-frame "K frame".
    /// Unit: \[N,N,N,Nm,Nm,Nm\].
    pub O_F_ext_hat_K: [f64; 6],
    /// ![^{K}F_{K,\text{ext}}](https://latex.codecogs.com/png.latex?^{K}F_{K,\text{ext}})
    ///
    /// Estimated external wrench (force, torque) acting on stiffness frame,
    /// expressed relative to the stiffness frame. See also @ref k-frame "K frame".
    /// Unit: \[N,N,N,Nm,Nm,Nm\].
    pub K_F_ext_hat_K: [f64; 6],
    /// ![{^OdP_{EE}}_{d}](https://latex.codecogs.com/png.latex?{^OdP_{EE}}_{d})
    ///
    /// Desired end effector twist in base frame.
    /// Unit: [m/s,m/s,m/s,rad/s,rad/s,rad/s]
    pub O_dP_EE_d: [f64; 6],
    /// ![{^OddP_O}](https://latex.codecogs.com/png.latex?{^OddP_O)
    ///
    /// Linear component of the acceleration of the robot's base, expressed in frame parallel to the
    /// base frame, i.e. the base's translational acceleration. If the base is resting
    /// this shows the direction of the gravity vector. It is hardcoded for now to `{0, 0, -9.81}`.
    pub O_ddP_O: [f64; 3],
    /// ![{^OT_{EE}}_{c}](https://latex.codecogs.com/png.latex?{^OT_{EE}}_{c})
    ///
    /// Last commanded end effector pose of motion generation in base frame.
    /// Pose is represented as a 4x4 matrix in column-major format.
    pub O_T_EE_c: [f64; 16],
    /// ![{^OdP_{EE}}_{c}](https://latex.codecogs.com/png.latex?{^OdP_{EE}}_{c})
    ///
    /// Last commanded end effector twist in base frame.
    /// Unit: [m/s,m/s,m/s,rad/s,rad/s,rad/s]
    pub O_dP_EE_c: [f64; 6],
    ///![{^OddP_{EE}}_{c}](https://latex.codecogs.com/png.latex?{^OddP_{EE}}_{c})
    ///
    /// Last commanded end effector acceleration in base frame.
    /// Unit:  [m/s^2,m/s^2,m/s^2,rad/s^2,rad/s^2,rad/s^2]
    pub O_ddP_EE_c: [f64; 6],
    /// ![\theta](https://latex.codecogs.com/png.latex?\theta)
    ///
    /// Motor position. Unit: \[rad\]
    pub theta: [f64; 7],
    /// ![\dot{\theta}](https://latex.codecogs.com/png.latex?\dot{\theta})
    ///
    /// Motor velocity. Unit: \[rad/s\]
    pub dtheta: [f64; 7],
    /// Current error state.
    pub current_errors: FrankaErrors,
    /// Contains the errors that aborted the previous motion
    pub last_motion_errors: FrankaErrors,
    /// Percentage of the last 100 control commands that were successfully received by the robot.
    ///
    /// Shows a value of zero if no control or motion generator loop is currently running.
    ///
    /// Range \[0,1\]
    pub control_command_success_rate: f64,
    /// Current robot mode.
    pub robot_mode: RobotMode,
    /// Strictly monotonically increasing timestamp since robot start.
    ///
    /// Inside of control loops "time_step" parameter of Robot::control can be used
    /// instead
    pub time: Duration,
}
impl From<RobotStateIntern> for RobotState {
    #[allow(non_snake_case)]
    fn from(robot_state: RobotStateIntern) -> Self {
        let O_T_EE = robot_state.O_T_EE;
        let O_T_EE_d = robot_state.O_T_EE_d;
        let F_T_NE = robot_state.F_T_NE;
        let NE_T_EE = robot_state.NE_T_EE;
        let F_T_EE = robot_state.F_T_EE;
        let EE_T_K = robot_state.EE_T_K;
        let m_ee = robot_state.m_ee;
        let F_x_Cee = robot_state.F_x_Cee;
        let I_ee = robot_state.I_ee;
        let m_load = robot_state.m_load;
        let F_x_Cload = robot_state.F_x_Cload;
        let I_load = robot_state.I_load;
        let m_total = robot_state.m_ee + robot_state.m_load;
        let F_x_Ctotal = combine_center_of_mass(
            robot_state.m_ee,
            robot_state.F_x_Cee,
            robot_state.m_load,
            robot_state.F_x_Cload,
        );
        let I_total = combine_inertia_tensor(
            robot_state.m_ee,
            robot_state.F_x_Cee,
            robot_state.I_ee,
            robot_state.m_load,
            robot_state.F_x_Cload,
            robot_state.I_load,
            m_total,
            F_x_Ctotal,
        );
        let elbow = robot_state.elbow;
        let elbow_d = robot_state.elbow_d;
        let elbow_c = robot_state.elbow_c;
        let delbow_c = robot_state.delbow_c;
        let ddelbow_c = robot_state.ddelbow_c;
        let tau_J = robot_state.tau_J;
        let tau_J_d = robot_state.tau_J_d;
        let dtau_J = robot_state.dtau_J;
        let q = robot_state.q;
        let dq = robot_state.dq;
        let q_d = robot_state.q_d;
        let dq_d = robot_state.dq_d;
        let ddq_d = robot_state.ddq_d;
        let joint_contact = robot_state.joint_contact;
        let cartesian_contact = robot_state.cartesian_contact;
        let joint_collision = robot_state.joint_collision;
        let cartesian_collision = robot_state.cartesian_collision;
        let tau_ext_hat_filtered = robot_state.tau_ext_hat_filtered;
        let O_F_ext_hat_K = robot_state.O_F_ext_hat_K;
        let K_F_ext_hat_K = robot_state.K_F_ext_hat_K;
        let O_dP_EE_d = robot_state.O_dP_EE_d;
        let O_ddP_O = robot_state.O_ddP_O;
        let O_T_EE_c = robot_state.O_T_EE_c;
        let O_dP_EE_c = robot_state.O_dP_EE_c;
        let O_ddP_EE_c = robot_state.O_ddP_EE_c;
        let theta = robot_state.theta;
        let dtheta = robot_state.dtheta;
        let control_command_success_rate = robot_state.control_command_success_rate;
        let time = Duration::from_millis(robot_state.message_id);
        let robot_mode = robot_state.robot_mode;
        let current_errors = FrankaErrors::new(robot_state.errors, FrankaErrorKind::Error);
        let last_motion_errors =
            FrankaErrors::new(robot_state.errors, FrankaErrorKind::ReflexReason);
        RobotState {
            O_T_EE,
            O_T_EE_d,
            F_T_EE,
            F_T_NE,
            NE_T_EE,
            EE_T_K,
            m_ee,
            I_ee,
            F_x_Cee,
            m_load,
            I_load,
            F_x_Cload,
            m_total,
            I_total,
            F_x_Ctotal,
            elbow,
            elbow_d,
            elbow_c,
            delbow_c,
            ddelbow_c,
            tau_J,
            tau_J_d,
            dtau_J,
            q,
            q_d,
            dq,
            dq_d,
            ddq_d,
            joint_contact,
            cartesian_contact,
            joint_collision,
            cartesian_collision,
            tau_ext_hat_filtered,
            O_F_ext_hat_K,
            K_F_ext_hat_K,
            O_dP_EE_d,
            O_ddP_O,
            O_T_EE_c,
            O_dP_EE_c,
            O_ddP_EE_c,
            theta,
            dtheta,
            current_errors,
            last_motion_errors,
            control_command_success_rate,
            robot_mode,
            time,
        }
    }
}

#[allow(non_snake_case, clippy::too_many_arguments)]
fn combine_inertia_tensor(
    m_ee: f64,
    F_x_Cee: [f64; 3],
    I_ee: [f64; 9],
    m_load: f64,
    F_x_Cload: [f64; 3],
    I_load: [f64; 9],
    m_total: f64,
    F_x_Ctotal: [f64; 3],
) -> [f64; 9] {
    let center_of_mass_ee = Vector3::from_column_slice(&F_x_Cee);
    let center_of_mass_load = Vector3::from_column_slice(&F_x_Cload);
    let center_of_mass_total = Vector3::from_column_slice(&F_x_Ctotal);

    let mut inertia_ee = Matrix3::from_column_slice(&I_ee);
    let mut inertia_load = Matrix3::from_column_slice(&I_load);

    if m_ee == 0. {
        inertia_ee = Matrix3::zeros();
    }
    if m_load == 0. {
        inertia_load = Matrix3::zeros();
    }
    let inertia_ee_flange = inertia_ee
        - m_ee
            * (skew_symmetric_matrix_from_vector(&center_of_mass_ee)
                * skew_symmetric_matrix_from_vector(&center_of_mass_ee));
    let inertia_load_flange = inertia_load
        - m_load
            * (skew_symmetric_matrix_from_vector(&center_of_mass_load)
                * skew_symmetric_matrix_from_vector(&center_of_mass_load));
    let inertia_total_flange = inertia_ee_flange + inertia_load_flange;

    let inertia_total: Matrix3<f64> = inertia_total_flange
        + m_total
            * (skew_symmetric_matrix_from_vector(&center_of_mass_total)
                * skew_symmetric_matrix_from_vector(&center_of_mass_total));
    let mut I_total = [0.; 9];
    for (i, &x) in inertia_total.as_slice().iter().enumerate() {
        I_total[i] = x;
    }
    I_total
}

#[allow(non_snake_case)]
fn combine_center_of_mass(
    m_ee: f64,
    F_x_Cee: [f64; 3],
    m_load: f64,
    F_x_Cload: [f64; 3],
) -> [f64; 3] {
    let mut F_x_Ctotal = [0.; 3];
    if m_ee + m_load > 0. {
        for i in 0..F_x_Ctotal.len() {
            F_x_Ctotal[i] = (m_ee * F_x_Cee[i] + m_load * F_x_Cload[i]) / (m_ee + m_load);
        }
    }
    F_x_Ctotal
}

fn skew_symmetric_matrix_from_vector(vector: &Vector3<f64>) -> Matrix3<f64> {
    Matrix3::new(
        0., -vector.z, vector.y, vector.z, 0., -vector.x, -vector.y, vector.x, 0.,
    )
}

#[cfg(test)]
mod tests {
    use crate::robot::robot_state::{combine_center_of_mass, combine_inertia_tensor};

    #[test]
    fn combine_com_test() {
        let m_ee = 0.;
        let m_load = 0.;
        let f_x_ctotal = combine_center_of_mass(m_ee, [0.; 3], m_load, [0.; 3]);
        f_x_ctotal
            .iter()
            .for_each(|&x| assert!(x.abs() < f64::EPSILON));

        let m_ee = 0.73;
        let m_load = 0.5;
        let f_x_cee = [-0.01, 0., -0.03];
        let i_ee = [0.001, 0.0, 0.0, 0.0, 0.0025, 0.0, 0.0, 0.0, 0.0017];
        let i_load = [0.001, 0.0, 0.0, 0.0, 0.025, 0.0, 0.0, 0.0, 0.3];
        let f_x_cload = [0.01, -0.2, 0.03];
        let m_total = m_ee + m_load;
        let f_x_ctotal = combine_center_of_mass(m_ee, f_x_cee, m_load, f_x_cload);
        let i_total = combine_inertia_tensor(
            m_ee, f_x_cee, i_ee, m_load, f_x_cload, i_load, m_total, f_x_ctotal,
        );
        let expected = [
            1.49382113821138e-02,
            1.18699186991870e-03,
            -3.56097560975610e-04,
            1.18699186991870e-03,
            2.36869918699187e-02,
            3.56097560975610e-03,
            -3.56097560975610e-04,
            3.56097560975610e-03,
            3.13688617886179e-01,
        ];
        i_total
            .iter()
            .zip(expected.iter())
            .take(3)
            .for_each(|(&x, &y)| assert!((x - y).abs() < 1e-14));
    }

    #[test]
    fn inertia_zero_test() {
        let m_ee = 0.;
        let m_load = 0.0;
        let f_x_cee = [-0.01, 0., -0.03];
        let i_ee = [0.001, 0.0, 0.0, 0.0, 0.0025, 0.0, 0.0, 0.0, 0.0017];
        let i_load = [0.001, 0.0, 0.0, 0.0, 0.025, 0.0, 0.0, 0.0, 0.3];
        let f_x_cload = [0.01, -0.2, 0.03];
        let m_total = m_ee + m_load;
        let f_x_ctotal = combine_center_of_mass(m_ee, f_x_cee, m_load, f_x_cload);
        let i_total = combine_inertia_tensor(
            m_ee, f_x_cee, i_ee, m_load, f_x_cload, i_load, m_total, f_x_ctotal,
        );
        let expected = [0.; 9];
        i_total
            .iter()
            .zip(expected.iter())
            .take(3)
            .for_each(|(&x, &y)| assert!((x - y).abs() < 1e-17));

        let m_ee = 0.;
        let m_load = 0.5;
        let f_x_cee = [-0.01, 0., -0.03];
        let i_ee = [0.001, 0.0, 0.0, 0.0, 0.0025, 0.0, 0.0, 0.0, 0.0017];
        let i_load = [0.001, 0.0, 0.0, 0.0, 0.025, 0.0, 0.0, 0.0, 0.3];
        let f_x_cload = [0.01, -0.2, 0.03];
        let m_total = m_ee + m_load;
        let f_x_ctotal = combine_center_of_mass(m_ee, f_x_cee, m_load, f_x_cload);
        let i_total = combine_inertia_tensor(
            m_ee, f_x_cee, i_ee, m_load, f_x_cload, i_load, m_total, f_x_ctotal,
        );
        let expected = i_load;
        i_total
            .iter()
            .zip(expected.iter())
            .take(3)
            .for_each(|(&x, &y)| assert!((x - y).abs() < 1e-17));

        let m_ee = 0.73;
        let m_load = 0.0;
        let f_x_cee = [-0.01, 0., -0.03];
        let i_ee = [0.001, 0.0, 0.0, 0.0, 0.0025, 0.0, 0.0, 0.0, 0.0017];
        let i_load = [0.001, 0.0, 0.0, 0.0, 0.025, 0.0, 0.0, 0.0, 0.3];
        let f_x_cload = [0.01, -0.2, 0.03];
        let m_total = m_ee + m_load;
        let f_x_ctotal = combine_center_of_mass(m_ee, f_x_cee, m_load, f_x_cload);
        let i_total = combine_inertia_tensor(
            m_ee, f_x_cee, i_ee, m_load, f_x_cload, i_load, m_total, f_x_ctotal,
        );
        let expected = i_ee;
        i_total
            .iter()
            .zip(expected.iter())
            .take(3)
            .for_each(|(&x, &y)| assert!((x - y).abs() < 1e-17));
    }
}