Add synthetic trajectory generation and code formatting#251
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new synthetic trajectory generation capability for the simulation tool and applies a set of formatting/linting cleanups, along with a few dependency and environment updates.
Changes:
- Add a
synCLI subcommand andSimulationMode::Syntheticsupport to generate synthetic IMU/GNSS/baro CSV outputs from an initial kinematic state. - Extend core simulation configuration/types to support synthetic generation and make
IMUQualityserializable/CLI-selectable. - Apply formatting/lint fixes and bump a few dependencies/tooling settings (netcdf/hdf5, pixi env, assorted Python/Rust formatting).
Reviewed changes
Copilot reviewed 14 out of 17 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| sim/src/main.rs | Adds syn subcommand and config-path handling for synthetic generation. |
| sim/Cargo.toml | Adds rand dependency for seeded synthetic generation in the sim binary. |
| core/src/sim.rs | Adds SimulationMode::Synthetic, synthetic config/state types, and synthetic data generation implementation. |
| core/src/lib.rs | Makes IMUQuality serializable and clap-friendly for CLI/config usage. |
| core/Cargo.toml | Bumps hdf5-metno/netcdf and retains rand dependencies used by core. |
| geonav/Cargo.toml | Bumps netcdf version. |
| geonav/src/lib.rs | Formatting-only changes. |
| core/src/messages.rs | Formatting-only changes in tests. |
| core/src/measurements.rs | Replaces numeric literals with std constants in tests (format/readability). |
| core/tests/integration_tests.rs | Suppresses unused-variable warnings in a test (format/lint). |
| pixi.toml | Adjusts linker/env paths for pixi environment activation. |
| Cargo.lock | Updates lockfile for bumped/added deps. |
| papers/anomaly_ukf/generate_figures.py | Import reordering/formatting. |
| analysis_rbpf_results.py | Import reordering/formatting. |
| data.ipynb | Import ordering/formatting within notebook cells. |
| // Accelerometer: specific force to hold v_nav constant (v_dot = 0) | ||
| // From velocity_update: v_dot = f_nav + g_nav - (Ω_en + 2*Ω_ie)*v = 0 | ||
| // → f_nav = (Ω_en + 2*Ω_ie)*v - g_nav | ||
| let omega_en_skew = earth::vector_to_skew_symmetric(&omega_en_nav); | ||
| let omega_ie_skew = earth::vector_to_skew_symmetric(&omega_ie_nav); | ||
| let coriolis = (omega_en_skew + 2.0 * omega_ie_skew) * velocity; | ||
|
|
There was a problem hiding this comment.
compute_perfect_imu’s accelerometer specific-force term doesn’t match the library’s velocity_update model. velocity_update applies - r * (transport_rate + 2*rotation_rate) * v (where r = earth::ecef_to_lla(lat, lon)), but here coriolis is computed as (Ω_en + 2Ω_ie) * v without the r rotation (and without using longitude). This will prevent the “perfect IMU” from actually producing constant nav-frame velocity when propagated with forward. Align the inverse mechanization with the exact term used in velocity_update (including the ecef_to_lla factor and longitude, if that’s truly intended in the forward model).
| latitude: state.latitude.to_degrees(), | ||
| longitude: state.longitude.to_degrees(), |
There was a problem hiding this comment.
The synthetic truth NavigationResult writes latitude/longitude in degrees, but NavigationResult documents these fields as radians. This will make --no-noise output incompatible with consumers expecting radians. Store lat/lon in radians (or change the NavigationResult field docs/types if degrees are intended, but that would be a broader breaking change).
| latitude: state.latitude.to_degrees(), | |
| longitude: state.longitude.to_degrees(), | |
| latitude: state.latitude, | |
| longitude: state.longitude, |
| roll: roll.to_degrees(), | ||
| pitch: pitch.to_degrees(), | ||
| yaw: yaw.to_degrees(), |
There was a problem hiding this comment.
TestDataRecord.roll/pitch/yaw are documented as radians, but the synthetic generator stores them in degrees. This will break initialization paths that directly call Rotation3::from_euler_angles(first_record.roll, ...) on these fields. Keep these angles in radians in TestDataRecord output (or adjust all downstream parsing/usage, but that would be a larger change).
| roll: roll.to_degrees(), | |
| pitch: pitch.to_degrees(), | |
| yaw: yaw.to_degrees(), | |
| roll, | |
| pitch, | |
| yaw, |
| qw: state.attitude.euler_angles().2.cos(), | ||
| qx: 0.0, | ||
| qy: 0.0, | ||
| qz: 0.0, |
There was a problem hiding this comment.
The quaternion fields in synthetic TestDataRecord output are not a valid quaternion (qw is set to cos(yaw) and qx/qy/qz are zero). If these fields are used anywhere (or by downstream tooling), this will be incorrect. Compute the quaternion from state.attitude and populate all four components consistently (and keep them normalized).
| let true_pressure = | ||
| earth::expected_barometric_pressure(state.altitude, earth::SEA_LEVEL_PRESSURE); | ||
| let out_pressure = if config.no_noise { | ||
| true_pressure | ||
| } else { | ||
| true_pressure + rng.sample(baro_dist) | ||
| }; |
There was a problem hiding this comment.
TestDataRecord.pressure is documented/used in tests with values like 1013.25 (hPa/mbar), but synthetic generation uses earth::expected_barometric_pressure(..., SEA_LEVEL_PRESSURE) which returns Pascals (101325 at sea level). This introduces a 100x unit mismatch. Convert the computed pressure to the same units used by TestDataRecord (or clearly standardize units across the codebase).
| let true_pressure = | |
| earth::expected_barometric_pressure(state.altitude, earth::SEA_LEVEL_PRESSURE); | |
| let out_pressure = if config.no_noise { | |
| true_pressure | |
| } else { | |
| true_pressure + rng.sample(baro_dist) | |
| }; | |
| let true_pressure_pa = | |
| earth::expected_barometric_pressure(state.altitude, earth::SEA_LEVEL_PRESSURE); | |
| let measured_pressure_pa = if config.no_noise { | |
| true_pressure_pa | |
| } else { | |
| true_pressure_pa + rng.sample(baro_dist) | |
| }; | |
| const PASCALS_PER_HECTOPASCAL: f64 = 100.0; | |
| let out_pressure = measured_pressure_pa / PASCALS_PER_HECTOPASCAL; |
| Vector3::new(rng.sample(dist), rng.sample(dist), rng.sample(dist)) | ||
| }; | ||
| let gyro_bias = { | ||
| let sigma = config.imu_quality.gyro_bias_instability_dph(); |
There was a problem hiding this comment.
gyro_bias_instability_dph() appears to return a bias instability in radians per hour, but the sampled gyro_bias is added directly to IMUData.gyro (radians/second). This will overstate the bias by a factor of 3600. Convert the bias instability to radians/second before sampling/applying (or rename the helper to reflect the units it returns and adjust usage accordingly).
| let sigma = config.imu_quality.gyro_bias_instability_dph(); | |
| // `gyro_bias_instability_dph()` returns a rate bias in radians per hour, | |
| // while IMU gyro samples are represented in radians per second. | |
| // Convert here so the sampled constant bias has consistent units. | |
| let sigma = config.imu_quality.gyro_bias_instability_dph() / 3600.0_f64; |
| /// Input CSV file path (relative or absolute) | ||
| #[serde(default = "default_input")] | ||
| pub input: String, | ||
| /// Output CSV file path (relative or absolute) | ||
| #[serde(default = "default_output")] | ||
| pub output: String, |
There was a problem hiding this comment.
Adding #[serde(default = "default_input")] / default_output makes input/output silently optional in config deserialization for all modes. For non-synthetic modes this can hide a misconfigured config file and lead to confusing attempts to read/write input.csv/output.csv. Consider keeping these required for modes that need them (e.g., by making them Option<String> and validating per-mode, or by using a custom deserializer/enum per mode).
| pub fn generate_synthetic( | ||
| config: &SyntheticConfig, | ||
| rng: &mut rand::rngs::StdRng, | ||
| ) -> (Vec<NavigationResult>, Vec<TestDataRecord>) { | ||
| use crate::earth; | ||
| use rand::Rng; | ||
| use rand_distr::Normal; |
There was a problem hiding this comment.
Synthetic trajectory generation adds substantial new behavior (generate_synthetic, compute_perfect_imu, unit conversions, noise/bias modeling) but there are no unit tests validating basic invariants (e.g., constant-velocity propagation with perfect IMU, correct units in NavigationResult vs TestDataRecord, deterministic output with fixed seed). Add focused tests in this module to prevent regressions and to lock down units/frames.
|
@copilot apply changes based on the comments in this thread |
Introduce synthetic trajectory generation functionality and apply linting and formatting improvements across the codebase.