Files
gps/soc/software/fusion/tests/fusion_synthetic_test.c
T

653 lines
23 KiB
C

#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "fusion/fusion_engine.h"
#define SYNTH_PI 3.14159265358979323846264338327950288
#define SYNTH_TIMEBASE_HZ 1000000u
#define SYNTH_IMU_HZ 200u
#define SYNTH_DURATION_S 60u
#define SYNTH_GPS_EVENT_CAPACITY 96u
typedef struct {
uint32_t state;
} synth_rng_t;
typedef struct {
double position_m[3];
double velocity_mps[3];
double yaw_rad;
} synth_truth_t;
typedef struct {
fusion_position_measurement_t measurement;
uint64_t delivery_ticks;
bool injected_outlier;
} synth_gps_event_t;
typedef struct {
double horizontal_rmse_m;
double vertical_rmse_m;
double final_horizontal_error_m;
double final_vertical_error_m;
double final_speed_mps;
double max_rejected_state_jump_m;
double covariance_symmetry_error;
uint64_t accepted_gps;
uint64_t rejected_gps;
uint64_t replay_count;
uint64_t replayed_imu_count;
uint64_t zero_updates;
unsigned int injected_outliers;
unsigned int rejected_injected_outliers;
unsigned int false_stationary_samples;
double moving_detection_time_s;
double stationary_reentry_time_s;
bool covariance_positive_semidefinite;
bool state_finite;
} synth_metrics_t;
typedef struct {
double final_horizontal_error_m;
double final_vertical_error_m;
double final_speed_mps;
uint64_t accepted_gps;
uint64_t zero_updates;
unsigned int nonstationary_samples;
bool covariance_positive_semidefinite;
bool state_finite;
} stationary_metrics_t;
static int failures;
#define CHECK(condition, message) do { \
if(!(condition)) { \
fprintf(stderr, "FAIL: %s\n", message); \
failures++; \
} \
} while(0)
static double square(double value)
{
return value * value;
}
static double norm2(double first, double second)
{
return sqrt(first * first + second * second);
}
static double norm3(const double value[3])
{
return sqrt(square(value[0]) + square(value[1]) + square(value[2]));
}
static uint32_t rng_next(synth_rng_t* rng)
{
uint32_t value = rng->state;
value ^= value << 13;
value ^= value >> 17;
value ^= value << 5;
rng->state = value;
return value;
}
static double rng_uniform(synth_rng_t* rng)
{
return ((double)rng_next(rng) + 0.5) / 4294967296.0;
}
static double rng_gaussian(synth_rng_t* rng)
{
double first = fmax(rng_uniform(rng), 1.0e-12);
double second = rng_uniform(rng);
return sqrt(-2.0 * log(first)) * cos(2.0 * SYNTH_PI * second);
}
static void trajectory_input(double time_s,
double acceleration_nav_mps2[3],
double* yaw_rate_rps)
{
memset(acceleration_nav_mps2, 0, 3u * sizeof(double));
*yaw_rate_rps = 0.0;
if((time_s >= 5.0) && (time_s < 7.0))
{
acceleration_nav_mps2[0] = 1.0;
}
else if((time_s >= 10.0) && (time_s < 12.0))
{
acceleration_nav_mps2[0] = -1.0;
}
else if((time_s >= 15.0) && (time_s < 17.0))
{
*yaw_rate_rps = 0.25 * SYNTH_PI;
}
else if((time_s >= 17.0) && (time_s < 19.0))
{
acceleration_nav_mps2[1] = 0.8;
}
else if((time_s >= 22.0) && (time_s < 24.0))
{
acceleration_nav_mps2[1] = -0.8;
}
}
static void truth_step(synth_truth_t* truth,
double dt_s,
const double acceleration_nav_mps2[3],
double yaw_rate_rps)
{
for(unsigned int axis = 0u ; axis < 3u ; axis++)
{
truth->position_m[axis] += truth->velocity_mps[axis] * dt_s +
0.5 * acceleration_nav_mps2[axis] * dt_s * dt_s;
truth->velocity_mps[axis] += acceleration_nav_mps2[axis] * dt_s;
}
truth->yaw_rad += yaw_rate_rps * dt_s;
}
static fusion_position_measurement_t make_position(
uint64_t timestamp_ticks,
const synth_truth_t* truth,
synth_rng_t* gps_rng,
bool outlier)
{
fusion_position_measurement_t position;
memset(&position, 0, sizeof(position));
position.timestamp_ticks = timestamp_ticks;
position.position_enu_m[0] = truth->position_m[0] + 1.2 * rng_gaussian(gps_rng);
position.position_enu_m[1] = truth->position_m[1] + 1.2 * rng_gaussian(gps_rng);
position.position_enu_m[2] = truth->position_m[2] + 2.0 * rng_gaussian(gps_rng);
if(outlier)
{
position.position_enu_m[0] += 150.0;
position.position_enu_m[1] -= 120.0;
position.position_enu_m[2] += 80.0;
}
position.covariance_enu_m2[0] = square(1.2);
position.covariance_enu_m2[4] = square(1.2);
position.covariance_enu_m2[8] = square(2.0);
position.source_quality = outlier ? 0.7 : 1.0;
position.source_valid = true;
return position;
}
static bool covariance_is_psd(const eskf_t* filter,
double* symmetry_error_out)
{
double lower[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
double max_symmetry_error = 0.0;
for(unsigned int row = 0u ; row < ESKF_ERROR_STATE_DIM ; row++)
{
for(unsigned int col = 0u ; col < ESKF_ERROR_STATE_DIM ; col++)
{
double error = fabs(filter->covariance[row][col] -
filter->covariance[col][row]);
if(error > max_symmetry_error)
{
max_symmetry_error = error;
}
}
}
if(symmetry_error_out != NULL)
{
*symmetry_error_out = max_symmetry_error;
}
/* A tiny diagonal jitter makes the semidefinite check numerically robust. */
for(unsigned int row = 0u ; row < ESKF_ERROR_STATE_DIM ; row++)
{
for(unsigned int col = 0u ; col <= row ; col++)
{
double value = filter->covariance[row][col];
if(row == col)
{
value += 1.0e-10;
}
for(unsigned int k = 0u ; k < col ; k++)
{
value -= lower[row][k] * lower[col][k];
}
if(row == col)
{
if(!isfinite(value) || (value <= 0.0))
{
return false;
}
lower[row][col] = sqrt(value);
}
else
{
lower[row][col] = value / lower[col][col];
}
}
}
return true;
}
static void initialize_synthetic_engine(fusion_engine_t* engine)
{
fusion_engine_config_t config;
fusion_engine_default_config(&config, SYNTH_TIMEBASE_HZ);
config.calibration_duration_s = 2.0;
config.calibration_min_samples = SYNTH_IMU_HZ;
CHECK(fusion_engine_init(engine, &config), "synthetic fusion engine initializes");
fusion_position_measurement_t origin;
memset(&origin, 0, sizeof(origin));
origin.timestamp_ticks = 0u;
origin.covariance_enu_m2[0] = square(1.2);
origin.covariance_enu_m2[4] = square(1.2);
origin.covariance_enu_m2[8] = square(2.0);
origin.source_quality = 1.0;
origin.source_valid = true;
CHECK(fusion_engine_push_position(engine, &origin, NULL, NULL),
"initial GPS is retained during calibration");
}
static synth_metrics_t run_dynamic_scenario(uint32_t scenario_seed)
{
static fusion_engine_t engine;
initialize_synthetic_engine(&engine);
synth_truth_t truth = {{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}, 0.0};
synth_rng_t imu_rng = {0x13579bdfu ^ (0x9e3779b9u * scenario_seed)};
synth_rng_t gps_rng = {0x2468ace1u ^ (0x85ebca6bu * scenario_seed)};
synth_rng_t timing_rng = {0xa5a5c3c3u ^ (0xc2b2ae35u * scenario_seed)};
const double accel_bias[3] = {0.030, -0.025, 0.060};
const double gyro_bias[3] = {0.0010, -0.0007, 0.0009};
const uint64_t imu_period_ticks = SYNTH_TIMEBASE_HZ / SYNTH_IMU_HZ;
const double dt_s = 1.0 / (double)SYNTH_IMU_HZ;
const uint64_t final_tick = (uint64_t)SYNTH_DURATION_S * SYNTH_TIMEBASE_HZ;
uint64_t next_gps_tick = 3000000u;
unsigned int gps_sequence = 0u;
synth_gps_event_t events[SYNTH_GPS_EVENT_CAPACITY];
unsigned int event_head = 0u;
unsigned int event_count = 0u;
double horizontal_error_sum = 0.0;
double vertical_error_sum = 0.0;
uint64_t error_sample_count = 0u;
synth_metrics_t metrics;
memset(&metrics, 0, sizeof(metrics));
metrics.moving_detection_time_s = NAN;
metrics.stationary_reentry_time_s = NAN;
#ifdef SYNTH_TRACE
fusion_motion_state_t traced_state = FUSION_MOTION_BOOTSTRAP;
#endif
for(uint64_t tick = 0u ; tick <= final_tick ; tick += imu_period_ticks)
{
double acceleration_nav[3] = {0.0, 0.0, 0.0};
double yaw_rate = 0.0;
if(tick != 0u)
{
double midpoint_s = ((double)tick - 0.5 * (double)imu_period_ticks) /
(double)SYNTH_TIMEBASE_HZ;
trajectory_input(midpoint_s, acceleration_nav, &yaw_rate);
truth_step(&truth, dt_s, acceleration_nav, yaw_rate);
}
double midpoint_yaw = truth.yaw_rad - 0.5 * yaw_rate * dt_s;
double cosine = cos(midpoint_yaw);
double sine = sin(midpoint_yaw);
double specific_force_nav[3] = {
acceleration_nav[0], acceleration_nav[1], engine.config.eskf.gravity_mps2
};
fusion_imu_measurement_t imu;
memset(&imu, 0, sizeof(imu));
imu.timestamp_ticks = tick;
imu.accel_mps2[0] = cosine * specific_force_nav[0] +
sine * specific_force_nav[1];
imu.accel_mps2[1] = -sine * specific_force_nav[0] +
cosine * specific_force_nav[1];
imu.accel_mps2[2] = specific_force_nav[2];
imu.gyro_rps[2] = yaw_rate;
for(unsigned int axis = 0u ; axis < 3u ; axis++)
{
imu.accel_mps2[axis] += accel_bias[axis] + 0.025 * rng_gaussian(&imu_rng);
imu.gyro_rps[axis] += gyro_bias[axis] + 0.0008 * rng_gaussian(&imu_rng);
}
imu.temperature_c = 25.0 + 0.5 * sin((double)tick / 10000000.0);
imu.sequence = (uint32_t)(tick / imu_period_ticks);
imu.vehicle_at_rest =
(norm3(truth.velocity_mps) < 0.05) &&
(norm3(acceleration_nav) < 0.05) && (fabs(yaw_rate) < 0.02);
CHECK(fusion_engine_push_imu(&engine, &imu), "dynamic synthetic IMU accepted");
if(engine.state == FUSION_ENGINE_FAULT)
{
break;
}
if((tick >= next_gps_tick) &&
((event_head + event_count) < SYNTH_GPS_EVENT_CAPACITY))
{
bool outlier = (gps_sequence == 8u) || (gps_sequence == 27u);
unsigned int index = event_head + event_count;
events[index].measurement = make_position(tick, &truth, &gps_rng, outlier);
double delay_s = 0.18 + 0.17 * rng_uniform(&timing_rng);
events[index].delivery_ticks = tick +
(uint64_t)(delay_s * (double)SYNTH_TIMEBASE_HZ + 0.5);
events[index].injected_outlier = outlier;
event_count++;
if(outlier)
{
metrics.injected_outliers++;
}
gps_sequence++;
double next_period_s = 0.80 + 0.40 * rng_uniform(&timing_rng);
next_gps_tick = tick +
(uint64_t)(next_period_s * (double)SYNTH_TIMEBASE_HZ + 0.5);
}
while((event_count != 0u) && (events[event_head].delivery_ticks <= tick))
{
double before[3];
memcpy(before, engine.current.filter.position_m, sizeof(before));
eskf_update_result_t result = ESKF_UPDATE_INVALID;
double nis = NAN;
CHECK(fusion_engine_push_position(&engine, &events[event_head].measurement,
&result, &nis),
"delayed asynchronous GPS is processed");
if(events[event_head].injected_outlier &&
(result == ESKF_UPDATE_REJECTED_NIS))
{
metrics.rejected_injected_outliers++;
double state_jump = norm2(
engine.current.filter.position_m[0] - before[0],
engine.current.filter.position_m[1] - before[1]);
if(state_jump > metrics.max_rejected_state_jump_m)
{
metrics.max_rejected_state_jump_m = state_jump;
}
}
event_head++;
event_count--;
}
if(engine.state == FUSION_ENGINE_RUNNING)
{
double time_s = (double)tick / (double)SYNTH_TIMEBASE_HZ;
fusion_motion_state_t state = engine.current.motion.state;
#ifdef SYNTH_TRACE
if(state != traced_state)
{
printf("TRACE t=%.3f state=%s speed=%.3f gnss=%u confirm=%u accel=%.3f jerk=%.3f\n",
time_s, fusion_motion_state_name(state),
eskf_speed_mps(&engine.current.filter),
(unsigned int)engine.current.gnss_evidence,
engine.current.gnss_moving_count,
engine.current.motion.last_linear_accel_norm_mps2,
engine.current.motion.last_jerk_norm_mps3);
traced_state = state;
}
#endif
if(isnan(metrics.moving_detection_time_s) && (time_s >= 5.0) &&
(state == FUSION_MOTION_MOVING))
{
metrics.moving_detection_time_s = time_s;
}
if((time_s >= 7.25) && (time_s <= 9.75) &&
(state == FUSION_MOTION_STATIONARY))
{
metrics.false_stationary_samples++;
}
if((time_s >= 19.25) && (time_s <= 21.75) &&
(state == FUSION_MOTION_STATIONARY))
{
metrics.false_stationary_samples++;
}
if(isnan(metrics.stationary_reentry_time_s) && (time_s >= 24.0) &&
(state == FUSION_MOTION_STATIONARY))
{
metrics.stationary_reentry_time_s = time_s;
}
if(time_s >= 3.0)
{
double east_error = engine.current.filter.position_m[0] - truth.position_m[0];
double north_error = engine.current.filter.position_m[1] - truth.position_m[1];
double up_error = engine.current.filter.position_m[2] - truth.position_m[2];
horizontal_error_sum += square(east_error) + square(north_error);
vertical_error_sum += square(up_error);
error_sample_count++;
}
}
}
metrics.accepted_gps = engine.diagnostics.position_accepted;
metrics.rejected_gps = engine.diagnostics.position_nis_rejected;
metrics.replay_count = engine.diagnostics.replay_count;
metrics.replayed_imu_count = engine.diagnostics.replayed_imu_count;
metrics.zero_updates = engine.diagnostics.zero_velocity_update_count;
metrics.state_finite = eskf_state_is_finite(&engine.current.filter);
metrics.covariance_positive_semidefinite = covariance_is_psd(
&engine.current.filter, &metrics.covariance_symmetry_error);
if(error_sample_count != 0u)
{
metrics.horizontal_rmse_m = sqrt(horizontal_error_sum / (double)error_sample_count);
metrics.vertical_rmse_m = sqrt(vertical_error_sum / (double)error_sample_count);
}
metrics.final_horizontal_error_m = norm2(
engine.current.filter.position_m[0] - truth.position_m[0],
engine.current.filter.position_m[1] - truth.position_m[1]);
metrics.final_vertical_error_m = fabs(
engine.current.filter.position_m[2] - truth.position_m[2]);
metrics.final_speed_mps = norm3(engine.current.filter.velocity_mps);
return metrics;
}
static void check_dynamic_metrics(const synth_metrics_t* metrics)
{
CHECK(metrics->state_finite, "60 s synthetic state remains finite");
CHECK(metrics->covariance_positive_semidefinite,
"60 s covariance remains positive semidefinite");
CHECK(metrics->covariance_symmetry_error < 1.0e-11,
"60 s covariance remains symmetric");
CHECK(isfinite(metrics->moving_detection_time_s) &&
(metrics->moving_detection_time_s <= 5.010),
"sudden acceleration is detected within two 200 Hz samples");
CHECK(metrics->false_stationary_samples == 0u,
"constant-velocity legs never enable stationary updates");
CHECK(isfinite(metrics->stationary_reentry_time_s) &&
(metrics->stationary_reentry_time_s >= 25.0) &&
(metrics->stationary_reentry_time_s <= 28.0),
"final stop enters stationary after hysteresis dwell");
CHECK(metrics->injected_outliers == 2u,
"two synthetic multipath-like GPS outliers were injected");
CHECK(metrics->rejected_injected_outliers == metrics->injected_outliers,
"NIS rejects every injected GPS outlier");
CHECK(metrics->max_rejected_state_jump_m < 1.0e-8,
"rejected delayed GPS does not perturb the current state");
CHECK(metrics->accepted_gps >= 45u,
"most asynchronous GPS measurements are accepted");
CHECK(metrics->rejected_gps >= metrics->injected_outliers,
"GPS reject diagnostics include injected outliers");
CHECK(metrics->replay_count >= 50u && metrics->replayed_imu_count > 1000u,
"delayed GPS exercises fixed-lag replay extensively");
CHECK(metrics->zero_updates > 150u,
"stationary periods apply PX4-rate 5 Hz ZUPT updates");
CHECK(metrics->horizontal_rmse_m < 2.0,
"bias/noise/delay trajectory horizontal RMSE is bounded");
CHECK(metrics->vertical_rmse_m < 2.5,
"bias/noise/delay trajectory vertical RMSE is bounded");
CHECK(metrics->final_horizontal_error_m < 1.5,
"final stationary horizontal error is bounded");
CHECK(metrics->final_vertical_error_m < 2.0,
"final stationary vertical error is bounded");
CHECK(metrics->final_speed_mps < 0.05,
"final stationary ZUPT speed is near zero");
}
static stationary_metrics_t run_stationary_500hz_scenario(void)
{
static fusion_engine_t engine;
fusion_engine_config_t config;
fusion_engine_default_config(&config, SYNTH_TIMEBASE_HZ);
config.calibration_duration_s = 2.0;
config.calibration_min_samples = 500u;
CHECK(fusion_engine_init(&engine, &config), "500 Hz stationary engine initializes");
fusion_position_measurement_t origin;
memset(&origin, 0, sizeof(origin));
origin.covariance_enu_m2[0] = square(0.8);
origin.covariance_enu_m2[4] = square(0.8);
origin.covariance_enu_m2[8] = square(1.5);
origin.source_quality = 1.0;
origin.source_valid = true;
CHECK(fusion_engine_push_position(&engine, &origin, NULL, NULL),
"500 Hz stationary origin accepted");
const uint64_t imu_period_ticks = SYNTH_TIMEBASE_HZ / 500u;
const uint64_t final_tick = 30000000u;
uint64_t next_gps_tick = 3000000u;
uint64_t delivery_tick = 0u;
bool gps_pending = false;
fusion_position_measurement_t pending_gps;
synth_rng_t imu_rng = {0x10203040u};
synth_rng_t gps_rng = {0x55667788u};
const double accel_bias[3] = {0.020, -0.015, 0.050};
const double gyro_bias[3] = {0.0008, -0.0005, 0.0006};
stationary_metrics_t metrics;
memset(&metrics, 0, sizeof(metrics));
for(uint64_t tick = 0u ; tick <= final_tick ; tick += imu_period_ticks)
{
fusion_imu_measurement_t imu;
memset(&imu, 0, sizeof(imu));
imu.timestamp_ticks = tick;
imu.accel_mps2[2] = config.eskf.gravity_mps2;
for(unsigned int axis = 0u ; axis < 3u ; axis++)
{
imu.accel_mps2[axis] += accel_bias[axis] + 0.020 * rng_gaussian(&imu_rng);
imu.gyro_rps[axis] = gyro_bias[axis] + 0.0006 * rng_gaussian(&imu_rng);
}
imu.temperature_c = 24.0;
imu.sequence = (uint32_t)(tick / imu_period_ticks);
imu.vehicle_at_rest = true;
CHECK(fusion_engine_push_imu(&engine, &imu), "500 Hz stationary IMU accepted");
if((tick >= next_gps_tick) && !gps_pending)
{
synth_truth_t stationary_truth = {{0.0, 0.0, 0.0},
{0.0, 0.0, 0.0}, 0.0};
pending_gps = make_position(tick, &stationary_truth, &gps_rng, false);
pending_gps.position_enu_m[0] *= 0.8 / 1.2;
pending_gps.position_enu_m[1] *= 0.8 / 1.2;
pending_gps.position_enu_m[2] *= 1.5 / 2.0;
pending_gps.covariance_enu_m2[0] = square(0.8);
pending_gps.covariance_enu_m2[4] = square(0.8);
pending_gps.covariance_enu_m2[8] = square(1.5);
delivery_tick = tick + 250000u;
gps_pending = true;
next_gps_tick += 1000000u;
}
if(gps_pending && (tick >= delivery_tick))
{
eskf_update_result_t result = ESKF_UPDATE_INVALID;
CHECK(fusion_engine_push_position(&engine, &pending_gps, &result, NULL),
"500 Hz delayed stationary GPS processed");
CHECK(result == ESKF_UPDATE_ACCEPTED,
"500 Hz stationary GPS passes NIS");
gps_pending = false;
}
if((tick >= 3000000u) && (engine.state == FUSION_ENGINE_RUNNING) &&
(engine.current.motion.state != FUSION_MOTION_STATIONARY))
{
metrics.nonstationary_samples++;
}
}
metrics.accepted_gps = engine.diagnostics.position_accepted;
metrics.zero_updates = engine.diagnostics.zero_velocity_update_count;
metrics.state_finite = eskf_state_is_finite(&engine.current.filter);
metrics.covariance_positive_semidefinite = covariance_is_psd(
&engine.current.filter, NULL);
metrics.final_horizontal_error_m = norm2(engine.current.filter.position_m[0],
engine.current.filter.position_m[1]);
metrics.final_vertical_error_m = fabs(engine.current.filter.position_m[2]);
metrics.final_speed_mps = norm3(engine.current.filter.velocity_mps);
return metrics;
}
static void check_stationary_metrics(const stationary_metrics_t* metrics)
{
CHECK(metrics->state_finite, "30 s / 500 Hz stationary state remains finite");
CHECK(metrics->covariance_positive_semidefinite,
"30 s / 500 Hz stationary covariance remains PSD");
CHECK(metrics->nonstationary_samples == 0u,
"30 s / 500 Hz noise never creates a false moving state");
CHECK(metrics->accepted_gps >= 26u,
"30 s / 500 Hz delayed stationary GPS remains accepted");
CHECK(metrics->zero_updates >= 120u,
"30 s / 500 Hz stationary run keeps PX4-rate ZUPT active");
CHECK(metrics->final_horizontal_error_m < 1.0,
"30 s / 500 Hz stationary horizontal error is bounded");
CHECK(metrics->final_vertical_error_m < 1.0,
"30 s / 500 Hz stationary vertical error is bounded");
CHECK(metrics->final_speed_mps < 0.02,
"30 s / 500 Hz stationary speed remains near zero");
}
int main(void)
{
synth_metrics_t metrics = run_dynamic_scenario(0u);
check_dynamic_metrics(&metrics);
double worst_horizontal_rmse_m = metrics.horizontal_rmse_m;
double worst_final_horizontal_error_m = metrics.final_horizontal_error_m;
double latest_stationary_reentry_s = metrics.stationary_reentry_time_s;
for(uint32_t scenario = 1u ; scenario < 8u ; scenario++)
{
synth_metrics_t trial = run_dynamic_scenario(scenario);
check_dynamic_metrics(&trial);
worst_horizontal_rmse_m = fmax(worst_horizontal_rmse_m,
trial.horizontal_rmse_m);
worst_final_horizontal_error_m = fmax(worst_final_horizontal_error_m,
trial.final_horizontal_error_m);
latest_stationary_reentry_s = fmax(latest_stationary_reentry_s,
trial.stationary_reentry_time_s);
}
stationary_metrics_t stationary = run_stationary_500hz_scenario();
check_stationary_metrics(&stationary);
printf("Synthetic 60 s asynchronous ESKF scenario\n");
printf(" position RMSE horizontal=%6.3f m vertical=%6.3f m\n",
metrics.horizontal_rmse_m, metrics.vertical_rmse_m);
printf(" final error horizontal=%6.3f m vertical=%6.3f m speed=%7.4f m/s\n",
metrics.final_horizontal_error_m, metrics.final_vertical_error_m,
metrics.final_speed_mps);
printf(" motion timing moving=%6.3f s stationary-again=%6.3f s false-still=%u\n",
metrics.moving_detection_time_s, metrics.stationary_reentry_time_s,
metrics.false_stationary_samples);
printf(" GPS/replay accepted=%llu rejected=%llu replay=%llu replayed-imu=%llu\n",
(unsigned long long)metrics.accepted_gps,
(unsigned long long)metrics.rejected_gps,
(unsigned long long)metrics.replay_count,
(unsigned long long)metrics.replayed_imu_count);
printf(" outlier isolation injected=%u rejected=%u max-state-jump=%.3e m\n",
metrics.injected_outliers, metrics.rejected_injected_outliers,
metrics.max_rejected_state_jump_m);
printf(" covariance PSD=%d symmetry-error=%.3e zero-updates=%llu\n",
metrics.covariance_positive_semidefinite ? 1 : 0,
metrics.covariance_symmetry_error,
(unsigned long long)metrics.zero_updates);
printf(" 8-seed robustness worst-h-rmse=%6.3f m worst-final-h=%6.3f m latest-stop=%6.3f s\n",
worst_horizontal_rmse_m, worst_final_horizontal_error_m,
latest_stationary_reentry_s);
printf("Synthetic 30 s stationary / 500 Hz scenario\n");
printf(" final error horizontal=%6.3f m vertical=%6.3f m speed=%7.4f m/s\n",
stationary.final_horizontal_error_m,
stationary.final_vertical_error_m,
stationary.final_speed_mps);
printf(" decisions false-moving-samples=%u GPS-accepted=%llu zero-updates=%llu\n",
stationary.nonstationary_samples,
(unsigned long long)stationary.accepted_gps,
(unsigned long long)stationary.zero_updates);
if(failures != 0)
{
fprintf(stderr, "%d synthetic fusion check(s) failed\n", failures);
return 1;
}
printf("PASS: production bare-metal ESKF sources passed the Linux/GCC scenario.\n");
return 0;
}