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

552 lines
20 KiB
C

#include "fusion/fusion_engine.h"
#include <errno.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TIMEBASE_HZ 1000000u
#define DURATION_S 50.0
#define PI 3.14159265358979323846264338327950288
typedef enum {
SCENARIO_STATIONARY = 0,
SCENARIO_PEDESTRIAN,
SCENARIO_CAR,
SCENARIO_AIRCRAFT,
SCENARIO_COUNT
} scenario_kind_t;
typedef struct {
double position_m[3];
double velocity_mps[3];
double yaw_rad;
} truth_t;
typedef struct {
double forward_accel_mps2;
double vertical_accel_mps2;
double yaw_rate_rps;
} command_t;
typedef struct {
uint32_t state;
} rng_t;
typedef struct {
double horizontal_rmse_m;
double final_horizontal_error_m;
double maximum_horizontal_error_m;
double maximum_burst_state_step_m;
uint64_t gps_accepted;
uint64_t gps_rejected;
uint64_t position_recoveries;
uint64_t zero_updates;
uint64_t zero_updates_after_startup;
bool finite;
bool recovered_after_burst;
} metrics_t;
static const char* scenario_name(scenario_kind_t scenario)
{
static const char* names[] = {"stationary", "pedestrian", "car", "aircraft"};
return names[(unsigned int)scenario];
}
static bool parse_scenario(const char* text, scenario_kind_t* scenario_out)
{
for(unsigned int index = 0u; index < SCENARIO_COUNT; index++)
{
if(strcmp(text, scenario_name((scenario_kind_t)index)) == 0)
{
*scenario_out = (scenario_kind_t)index;
return true;
}
}
return false;
}
static double square(double value)
{
return value * value;
}
static double rng_uniform(rng_t* rng)
{
rng->state = 1664525u * rng->state + 1013904223u;
return ((double)(rng->state >> 8u) + 0.5) / 16777216.0;
}
static double rng_normal(rng_t* rng)
{
double u1 = fmax(rng_uniform(rng), 1.0e-12);
double u2 = rng_uniform(rng);
return sqrt(-2.0 * log(u1)) * cos(2.0 * PI * u2);
}
static command_t command_at(scenario_kind_t scenario, double time_s)
{
command_t command = {0.0, 0.0, 0.0};
if(scenario == SCENARIO_PEDESTRIAN)
{
if((time_s >= 4.0) && (time_s < 6.0)) command.forward_accel_mps2 = 0.75;
else if((time_s >= 14.0) && (time_s < 16.0)) command.forward_accel_mps2 = -0.75;
else if((time_s >= 21.0) && (time_s < 23.0)) command.forward_accel_mps2 = 0.60;
else if((time_s >= 31.0) && (time_s < 33.0)) command.forward_accel_mps2 = -0.60;
if((time_s >= 24.0) && (time_s < 29.0)) command.yaw_rate_rps = 0.10;
}
else if(scenario == SCENARIO_CAR)
{
if((time_s >= 4.0) && (time_s < 9.0)) command.forward_accel_mps2 = 4.0;
else if((time_s >= 15.0) && (time_s < 18.0)) command.forward_accel_mps2 = -20.0 / 3.0;
else if((time_s >= 25.0) && (time_s < 29.0)) command.forward_accel_mps2 = 5.0;
else if((time_s >= 39.0) && (time_s < 42.0)) command.forward_accel_mps2 = -20.0 / 3.0;
if((time_s >= 31.0) && (time_s < 36.0)) command.yaw_rate_rps = 0.12;
}
else if(scenario == SCENARIO_AIRCRAFT)
{
if((time_s >= 4.0) && (time_s < 14.0)) command.forward_accel_mps2 = 8.0;
else if((time_s >= 26.0) && (time_s < 29.0)) command.forward_accel_mps2 = 4.0;
else if((time_s >= 42.0) && (time_s < 47.0)) command.forward_accel_mps2 = -6.0;
if((time_s >= 6.0) && (time_s < 10.0)) command.vertical_accel_mps2 = 2.0;
else if((time_s >= 18.0) && (time_s < 22.0)) command.vertical_accel_mps2 = -2.0;
if((time_s >= 20.0) && (time_s < 25.0)) command.yaw_rate_rps = 0.20;
else if((time_s >= 34.0) && (time_s < 36.0)) command.yaw_rate_rps = -0.35;
}
return command;
}
static void attitude_at(scenario_kind_t scenario, double time_s,
const truth_t* truth, double euler_rad[3],
double body_rate_rps[3])
{
memset(euler_rad, 0, 3u * sizeof(double));
memset(body_rate_rps, 0, 3u * sizeof(double));
euler_rad[2] = truth->yaw_rad;
command_t command = command_at(scenario, time_s);
body_rate_rps[2] = command.yaw_rate_rps;
if(scenario != SCENARIO_STATIONARY)
{
return;
}
const double roll_max = 20.0 * PI / 180.0;
const double pitch_max = 12.0 * PI / 180.0;
if((time_s >= 10.0) && (time_s < 11.0))
{
euler_rad[0] = roll_max * (time_s - 10.0);
body_rate_rps[0] = roll_max;
}
else if((time_s >= 11.0) && (time_s < 13.0)) euler_rad[0] = roll_max;
else if((time_s >= 13.0) && (time_s < 14.0))
{
euler_rad[0] = roll_max * (14.0 - time_s);
body_rate_rps[0] = -roll_max;
}
if((time_s >= 18.0) && (time_s < 19.0))
{
euler_rad[1] = pitch_max * (time_s - 18.0);
body_rate_rps[1] = pitch_max;
}
else if((time_s >= 19.0) && (time_s < 21.0)) euler_rad[1] = pitch_max;
else if((time_s >= 21.0) && (time_s < 22.0))
{
euler_rad[1] = pitch_max * (22.0 - time_s);
body_rate_rps[1] = -pitch_max;
}
}
static void rotation_body_to_nav(const double euler[3], double rotation[3][3])
{
double cr = cos(euler[0]);
double sr = sin(euler[0]);
double cp = cos(euler[1]);
double sp = sin(euler[1]);
double cy = cos(euler[2]);
double sy = sin(euler[2]);
rotation[0][0] = cy * cp;
rotation[0][1] = cy * sp * sr - sy * cr;
rotation[0][2] = cy * sp * cr + sy * sr;
rotation[1][0] = sy * cp;
rotation[1][1] = sy * sp * sr + cy * cr;
rotation[1][2] = sy * sp * cr - cy * sr;
rotation[2][0] = -sp;
rotation[2][1] = cp * sr;
rotation[2][2] = cp * cr;
}
static void propagate_truth(truth_t* truth, scenario_kind_t scenario,
double start_s, double dt_s)
{
command_t command = command_at(scenario, start_s + 0.5 * dt_s);
double speed = hypot(truth->velocity_mps[0], truth->velocity_mps[1]);
double yaw_mid = truth->yaw_rad + 0.5 * command.yaw_rate_rps * dt_s;
double acceleration[3] = {
command.forward_accel_mps2 * cos(yaw_mid) - speed * command.yaw_rate_rps * sin(yaw_mid),
command.forward_accel_mps2 * sin(yaw_mid) + speed * command.yaw_rate_rps * cos(yaw_mid),
command.vertical_accel_mps2
};
for(unsigned int axis = 0u; axis < 3u; axis++)
{
truth->position_m[axis] += truth->velocity_mps[axis] * dt_s +
0.5 * acceleration[axis] * dt_s * dt_s;
truth->velocity_mps[axis] += acceleration[axis] * dt_s;
}
truth->yaw_rad += command.yaw_rate_rps * dt_s;
}
static bool externally_at_rest(scenario_kind_t scenario, double time_s)
{
if(time_s < 3.5) return true;
if(scenario != SCENARIO_STATIONARY) return false;
/* An external landed/rest supervisor owns this decision. Attitude changes
* and the vibration interval revoke the constraint; quiet IMU alone never
* invents rest for the moving scenarios. */
return !(((time_s >= 9.8) && (time_s < 15.0)) ||
((time_s >= 17.8) && (time_s < 23.0)) ||
((time_s >= 27.0) && (time_s < 30.0)));
}
static fusion_imu_measurement_t make_imu(scenario_kind_t scenario,
const truth_t* truth, double time_s,
uint64_t ticks, double gravity,
unsigned int sequence, rng_t* rng)
{
command_t command = command_at(scenario, time_s);
double speed = hypot(truth->velocity_mps[0], truth->velocity_mps[1]);
double acceleration_nav[3] = {
command.forward_accel_mps2 * cos(truth->yaw_rad) -
speed * command.yaw_rate_rps * sin(truth->yaw_rad),
command.forward_accel_mps2 * sin(truth->yaw_rad) +
speed * command.yaw_rate_rps * cos(truth->yaw_rad),
command.vertical_accel_mps2 + gravity
};
double euler[3];
double body_rate[3];
double rotation[3][3];
attitude_at(scenario, time_s, truth, euler, body_rate);
rotation_body_to_nav(euler, rotation);
fusion_imu_measurement_t imu;
memset(&imu, 0, sizeof(imu));
imu.timestamp_ticks = ticks;
for(unsigned int body = 0u; body < 3u; body++)
{
for(unsigned int nav = 0u; nav < 3u; nav++)
{
imu.accel_mps2[body] += rotation[nav][body] * acceleration_nav[nav];
}
static const double accel_bias[3] = {0.035, -0.025, 0.055};
static const double gyro_bias[3] = {0.0008, -0.0006, 0.0007};
imu.accel_mps2[body] += accel_bias[body] + 0.012 * rng_normal(rng);
imu.gyro_rps[body] = body_rate[body] + gyro_bias[body] +
0.00035 * rng_normal(rng);
}
if((scenario == SCENARIO_STATIONARY) &&
(time_s >= 27.0) && (time_s < 29.0))
{
imu.accel_mps2[0] += 0.80 * sin(2.0 * PI * 7.0 * time_s);
imu.accel_mps2[1] += 0.55 * sin(2.0 * PI * 11.0 * time_s);
imu.gyro_rps[2] += 0.035 * sin(2.0 * PI * 5.0 * time_s);
}
imu.temperature_c = 25.0 + 0.5 * sin(0.08 * time_s);
imu.sequence = sequence;
imu.vehicle_at_rest = externally_at_rest(scenario, time_s);
return imu;
}
static fusion_position_measurement_t make_gps(const truth_t* truth,
double time_s, uint64_t ticks,
rng_t* rng, double gps_position[3])
{
double horizontal_std = time_s < 8.0 ? 8.0 : 4.0;
double vertical_std = time_s < 8.0 ? 12.0 : 7.0;
for(unsigned int axis = 0u; axis < 3u; axis++)
{
double std = axis < 2u ? horizontal_std : vertical_std;
gps_position[axis] = truth->position_m[axis] + std * rng_normal(rng);
}
if((time_s >= 32.0) && (time_s < 34.0))
{
gps_position[0] += 130.0;
gps_position[1] -= 90.0;
gps_position[2] += 55.0;
}
fusion_position_measurement_t gps;
memset(&gps, 0, sizeof(gps));
gps.timestamp_ticks = ticks;
memcpy(gps.position_enu_m, gps_position, sizeof(gps.position_enu_m));
gps.covariance_enu_m2[0] = square(horizontal_std);
gps.covariance_enu_m2[4] = square(horizontal_std);
gps.covariance_enu_m2[8] = square(vertical_std);
/* Exactly four satellites have zero residual degrees of freedom. This is
* therefore a valid WLS output with covariance but no redundant FDE test. */
gps.source_quality = 100.0;
gps.source_valid = true;
return gps;
}
static void local_to_geodetic(const double position[3], double* latitude_deg,
double* longitude_deg, double* height_m)
{
const double origin_latitude_deg = 40.78925034214207;
const double origin_longitude_deg = 29.46277530841728;
const double origin_height_m = 100.0;
const double latitude_rad = origin_latitude_deg * PI / 180.0;
*latitude_deg = origin_latitude_deg + position[1] / 6378137.0 * 180.0 / PI;
*longitude_deg = origin_longitude_deg +
position[0] / (6378137.0 * cos(latitude_rad)) * 180.0 / PI;
*height_m = origin_height_m + position[2];
}
static void write_location(FILE* log_file, const char* source,
scenario_kind_t scenario, double time_s,
const double position[3], unsigned int gps_ms,
unsigned int imu_ms)
{
if(log_file == NULL) return;
double latitude;
double longitude;
double height;
local_to_geodetic(position, &latitude, &longitude, &height);
fprintf(log_file,
"[LOCATION] source=%s scenario=%s t=%.3f lat=%.12f lon=%.12f "
"height=%.3f gdop=3.2 pdop=2.6 hdop=2.0 vdop=1.7 tdop=1.8 "
"tracked=4 used=4 gps_ms=%u imu_ms=%u\n",
source, scenario_name(scenario), time_s, latitude, longitude,
height, gps_ms, imu_ms);
}
static metrics_t run_case(scenario_kind_t scenario, unsigned int gps_ms,
unsigned int imu_ms, FILE* log_file)
{
static fusion_engine_t engine;
memset(&engine, 0, sizeof(engine));
fusion_engine_config_t config;
fusion_engine_default_config(&config, TIMEBASE_HZ);
config.calibration_duration_s = 2.0;
/* Calibration is time-based for asynchronous sensors; retain enough samples
* to estimate variance without silently assuming a particular ODR. */
config.calibration_min_samples = 2u * 1000u / imu_ms + 1u;
metrics_t metrics;
memset(&metrics, 0, sizeof(metrics));
if(!fusion_engine_init(&engine, &config)) return metrics;
truth_t truth;
memset(&truth, 0, sizeof(truth));
rng_t imu_rng = {0x7a31f20du ^ ((uint32_t)scenario << 24u) ^ imu_ms};
rng_t gps_rng = {0xb41d93c7u ^ ((uint32_t)scenario << 20u) ^ gps_ms};
uint64_t imu_period_ticks = (uint64_t)imu_ms * 1000u;
uint64_t gps_period_ticks = (uint64_t)gps_ms * 1000u;
uint64_t next_imu_ticks = 0u;
uint64_t next_gps_ticks = 0u;
uint64_t next_plot_ticks = 0u;
uint64_t end_ticks = (uint64_t)(DURATION_S * TIMEBASE_HZ);
uint64_t last_ticks = 0u;
unsigned int imu_sequence = 0u;
double error_squared_sum = 0.0;
uint64_t error_count = 0u;
uint64_t accepted_at_burst_end = 0u;
uint64_t zero_updates_at_startup_end = 0u;
double previous_horizontal[2] = {0.0, 0.0};
bool have_previous = false;
if(log_file != NULL)
{
fprintf(log_file, "# ESKF synthetic replay; blue=GPS red=IMU-corrected green=truth\n");
fprintf(log_file, "# scenario=%s gps_ms=%u imu_ms=%u satellites=4\n",
scenario_name(scenario), gps_ms, imu_ms);
}
while(true)
{
uint64_t ticks = next_imu_ticks;
if(next_gps_ticks < ticks) ticks = next_gps_ticks;
if(next_plot_ticks < ticks) ticks = next_plot_ticks;
if(ticks > end_ticks) break;
double start_s = (double)last_ticks / TIMEBASE_HZ;
double dt_s = (double)(ticks - last_ticks) / TIMEBASE_HZ;
propagate_truth(&truth, scenario, start_s, dt_s);
last_ticks = ticks;
double time_s = (double)ticks / TIMEBASE_HZ;
if(ticks == next_gps_ticks)
{
/* A 3 s dropout exercises asynchronous dead reckoning. */
if(!((time_s >= 25.0) && (time_s < 28.0)))
{
double gps_position[3];
fusion_position_measurement_t gps = make_gps(
&truth, time_s, ticks, &gps_rng, gps_position);
eskf_update_result_t result = ESKF_UPDATE_INVALID;
(void)fusion_engine_push_position(&engine, &gps, &result, NULL);
write_location(log_file, "gps", scenario, time_s, gps_position,
gps_ms, imu_ms);
}
next_gps_ticks += gps_period_ticks;
}
if(ticks == next_imu_ticks)
{
fusion_imu_measurement_t imu = make_imu(
scenario, &truth, time_s, ticks, config.eskf.gravity_mps2,
imu_sequence++, &imu_rng);
if(!fusion_engine_push_imu(&engine, &imu)) break;
next_imu_ticks += imu_period_ticks;
}
if(engine.state == FUSION_ENGINE_RUNNING)
{
if((time_s >= 4.0) && (zero_updates_at_startup_end == 0u))
zero_updates_at_startup_end =
engine.diagnostics.zero_velocity_update_count;
double horizontal_error = hypot(
engine.current.filter.position_m[0] - truth.position_m[0],
engine.current.filter.position_m[1] - truth.position_m[1]);
metrics.maximum_horizontal_error_m = fmax(
metrics.maximum_horizontal_error_m, horizontal_error);
if(time_s >= 8.0)
{
error_squared_sum += square(horizontal_error);
error_count++;
}
if(have_previous && (time_s >= 32.0) && (time_s < 34.2))
{
double step = hypot(
engine.current.filter.position_m[0] - previous_horizontal[0],
engine.current.filter.position_m[1] - previous_horizontal[1]);
metrics.maximum_burst_state_step_m = fmax(
metrics.maximum_burst_state_step_m, step);
}
previous_horizontal[0] = engine.current.filter.position_m[0];
previous_horizontal[1] = engine.current.filter.position_m[1];
have_previous = true;
if((time_s >= 34.0) && (accepted_at_burst_end == 0u))
accepted_at_burst_end = engine.diagnostics.position_accepted;
}
if(ticks == next_plot_ticks)
{
write_location(log_file, "trajectory", scenario, time_s,
truth.position_m, gps_ms, imu_ms);
if(engine.state == FUSION_ENGINE_RUNNING)
write_location(log_file, "imu_corrected", scenario, time_s,
engine.current.filter.position_m, gps_ms, imu_ms);
next_plot_ticks += 200000u;
}
}
double final_error = hypot(
engine.current.filter.position_m[0] - truth.position_m[0],
engine.current.filter.position_m[1] - truth.position_m[1]);
metrics.horizontal_rmse_m = error_count > 0u
? sqrt(error_squared_sum / (double)error_count) : INFINITY;
metrics.final_horizontal_error_m = final_error;
metrics.gps_accepted = engine.diagnostics.position_accepted;
metrics.gps_rejected = engine.diagnostics.position_nis_rejected;
metrics.position_recoveries = engine.diagnostics.position_recovery_count;
metrics.zero_updates = engine.diagnostics.zero_velocity_update_count;
metrics.zero_updates_after_startup = metrics.zero_updates >= zero_updates_at_startup_end
? metrics.zero_updates - zero_updates_at_startup_end : metrics.zero_updates;
metrics.recovered_after_burst = engine.diagnostics.position_accepted >
accepted_at_burst_end;
metrics.finite = (engine.state == FUSION_ENGINE_RUNNING) &&
eskf_state_is_finite(&engine.current.filter) && isfinite(final_error);
return metrics;
}
static void print_metrics(scenario_kind_t scenario, unsigned int gps_ms,
unsigned int imu_ms, const metrics_t* metrics)
{
printf("%-10s GPS=%4ums IMU=%3ums rmse=%7.2fm final=%7.2fm max=%7.2fm "
"burst-step=%6.2fm gps=%3llu/%2llu recovery=%llu zupt=%llu(+%llu) finite=%u\n",
scenario_name(scenario), gps_ms, imu_ms, metrics->horizontal_rmse_m,
metrics->final_horizontal_error_m, metrics->maximum_horizontal_error_m,
metrics->maximum_burst_state_step_m,
(unsigned long long)metrics->gps_accepted,
(unsigned long long)metrics->gps_rejected,
(unsigned long long)metrics->position_recoveries,
(unsigned long long)metrics->zero_updates,
(unsigned long long)metrics->zero_updates_after_startup,
metrics->finite ? 1u : 0u);
}
static int run_matrix(void)
{
static const unsigned int gps_periods_ms[] = {200u, 500u, 1000u};
static const unsigned int imu_periods_ms[] = {19u, 20u, 200u, 300u};
bool passed = true;
printf("Four-satellite asynchronous platform matrix; 3 s outage and 2 s coherent GNSS fault\n");
for(unsigned int scenario = 0u; scenario < SCENARIO_COUNT; scenario++)
{
for(unsigned int gps_index = 0u; gps_index < 3u; gps_index++)
{
for(unsigned int imu_index = 0u; imu_index < 4u; imu_index++)
{
metrics_t metrics = run_case((scenario_kind_t)scenario,
gps_periods_ms[gps_index], imu_periods_ms[imu_index], NULL);
print_metrics((scenario_kind_t)scenario, gps_periods_ms[gps_index],
imu_periods_ms[imu_index], &metrics);
bool common_ok = metrics.finite && metrics.recovered_after_burst &&
(metrics.position_recoveries == 0u) &&
(metrics.maximum_burst_state_step_m < 40.0);
bool rest_ok = (scenario == SCENARIO_STATIONARY)
? (metrics.zero_updates_after_startup > 0u)
: (metrics.zero_updates_after_startup == 0u);
bool production_rate_ok = (imu_periods_ms[imu_index] > 20u) ||
(metrics.final_horizontal_error_m < 25.0);
passed = passed && common_ok && rest_ok && production_rate_ok;
}
}
}
if(!passed)
{
fprintf(stderr, "FAIL: platform scenario matrix contract.\n");
return 1;
}
printf("PASS: 19/20 ms production rates stayed bounded; 200/300 ms coarse IMU "
"rates remained finite and their dynamic accuracy loss is reported, not hidden.\n");
return 0;
}
int main(int argc, char** argv)
{
if(argc == 1) return run_matrix();
if((argc == 6) && (strcmp(argv[1], "--log") == 0))
{
scenario_kind_t scenario;
if(!parse_scenario(argv[2], &scenario))
{
fprintf(stderr, "unknown scenario: %s\n", argv[2]);
return 2;
}
unsigned int gps_ms = (unsigned int)strtoul(argv[3], NULL, 10);
unsigned int imu_ms = (unsigned int)strtoul(argv[4], NULL, 10);
if(!((gps_ms == 200u) || (gps_ms == 500u) || (gps_ms == 1000u)) ||
!((imu_ms == 19u) || (imu_ms == 20u) ||
(imu_ms == 200u) || (imu_ms == 300u)))
{
fprintf(stderr, "supported GPS: 200/500/1000 ms; IMU: 19/20/200/300 ms\n");
return 2;
}
FILE* log_file = fopen(argv[5], "w");
if(log_file == NULL)
{
fprintf(stderr, "cannot create %s: %s\n", argv[5], strerror(errno));
return 2;
}
metrics_t metrics = run_case(scenario, gps_ms, imu_ms, log_file);
if(fclose(log_file) != 0)
{
fprintf(stderr, "cannot close %s\n", argv[5]);
return 2;
}
print_metrics(scenario, gps_ms, imu_ms, &metrics);
printf("wrote replay log: %s\n", argv[5]);
return metrics.finite ? 0 : 1;
}
fprintf(stderr, "usage: %s [--log stationary|pedestrian|car|aircraft "
"200|500|1000 19|20|200|300 output.log]\n", argv[0]);
return 2;
}