Skip to content

Commit 33f93e5

Browse files
authored
Merge pull request #82 from jbrodovsky:jbrodovsky/issue42
Manual recovery method for covariance square root crashes
2 parents 3ec6b49 + e6714a2 commit 33f93e5

10 files changed

Lines changed: 1994 additions & 487 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/strapdown_py/dist
66
/core/*.csv
77
/core/db
8+
/out
89

910
# pixi environments
1011
.pixi

Cargo.lock

Lines changed: 30 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "strapdown-rs"
33
authors = ["James Brodovsky"]
44
version = "0.3.7"
55
edition = "2024"
6-
description = "A toolbox for building and analyzing strapdown inertial navigation systems."
6+
description = "A toolbox for building and analyzing strapdown inertial navigation systems as well as simulating various scenarios of GNSS signal degradation."
77
license = "MIT"
88
readme = "README.md"
99
homepage = "https://www.strapdown.rs"
@@ -34,6 +34,7 @@ rand_distr = "0.5.1"
3434
serde = { version = "1.0.219", features = ["derive"] }
3535
clap = { version = "4.5.4", features = ["derive"] }
3636
world_magnetic_model = "0.4.0"
37+
anyhow = "1.0.99"
3738

3839
[dev-dependencies]
3940
assert_approx_eq = "1.1.0"

core/src/earth.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,90 @@ pub fn ecef_to_lla(latitude: &f64, longitude: &f64) -> Matrix3<f64> {
315315
pub fn lla_to_ecef(latitude: &f64, longitude: &f64) -> Matrix3<f64> {
316316
ecef_to_lla(latitude, longitude).transpose()
317317
}
318+
/// Convert NED displacements (north/east, meters) into changes in latitude/longitude (radians).
319+
///
320+
/// Uses a simplified WGS84 ellipsoidal Earth model to compute radii of curvature
321+
/// in the meridian and prime vertical, then converts linear displacements to
322+
/// angular offsets.
323+
///
324+
/// ## Arguments
325+
/// - `lat_rad`: Current geodetic latitude in **radians**.
326+
/// - `alt_m`: Current altitude above the ellipsoid in meters.
327+
/// - `d_n`: Northward displacement in meters.
328+
/// - `d_e`: Eastward displacement in meters.
329+
///
330+
/// ## Returns
331+
/// A tuple `(dlat, dlon)`:
332+
/// - `dlat`: Change in latitude (radians).
333+
/// - `dlon`: Change in longitude (radians).
334+
///
335+
/// ## Notes
336+
/// - A small clamp is applied to `cos(lat)` to avoid division by zero at the poles.
337+
/// - This is an approximation sufficient for small offsets (meters to kilometers).
338+
///
339+
/// ## Example
340+
///
341+
/// ```
342+
/// let lat0 = 40.0_f64.to_radians();
343+
/// let alt = 0.0;
344+
/// // Move 100 m north and 50 m east
345+
/// let (dlat, dlon) = strapdown::earth::meters_ned_to_dlat_dlon(lat0, alt, 100.0, 50.0);
346+
/// println!("Δlat = {} rad, Δlon = {} rad", dlat, dlon);
347+
/// ```
348+
pub fn meters_ned_to_dlat_dlon(lat_rad: f64, alt_m: f64, d_n: f64, d_e: f64) -> (f64, f64) {
349+
// WGS84 principal radii (approx)
350+
let a = 6378137.0;
351+
let e2 = 6.69437999014e-3;
352+
let sin_lat = lat_rad.sin();
353+
let rn = a / (1.0 - e2 * sin_lat * sin_lat).sqrt();
354+
let re = rn * (1.0 - e2) / (1.0 - e2 * sin_lat * sin_lat);
355+
let dlat = d_n / (rn + alt_m);
356+
let dlon = d_e / ((re + alt_m) * lat_rad.cos().max(1e-6));
357+
(dlat, dlon)
358+
}
359+
/// Compute the great-circle distance between two geodetic coordinates
360+
/// using the haversine formula.
361+
///
362+
/// This assumes a spherical Earth with mean radius `R = 6,371,000 m`.
363+
/// For higher precision you can adapt this to use ellipsoidal formulas,
364+
/// but haversine is usually accurate to within ~0.5% for most navigation
365+
/// and simulation purposes.
366+
///
367+
/// ## Arguments
368+
/// - `lat1_rad`: Latitude of first point (radians).
369+
/// - `lon1_rad`: Longitude of first point (radians).
370+
/// - `lat2_rad`: Latitude of second point (radians).
371+
/// - `lon2_rad`: Longitude of second point (radians).
372+
///
373+
/// ## Returns
374+
/// Great-circle distance between the two points, in meters.
375+
///
376+
/// ## Example
377+
/// ```
378+
/// let d = strapdown::earth::haversine_distance(
379+
/// 40.0_f64.to_radians(), -75.0_f64.to_radians(),
380+
/// 41.0_f64.to_radians(), -74.0_f64.to_radians(),
381+
/// );
382+
/// println!("Distance: {:.1} km", d / 1000.0);
383+
/// ```
384+
pub fn haversine_distance(
385+
lat1_rad: f64,
386+
lon1_rad: f64,
387+
lat2_rad: f64,
388+
lon2_rad: f64,
389+
) -> f64 {
390+
let dlat = lat2_rad - lat1_rad;
391+
let dlon = lon2_rad - lon1_rad;
392+
393+
let a = (dlat / 2.0).sin().powi(2)
394+
+ lat1_rad.cos() * lat2_rad.cos() * (dlon / 2.0).sin().powi(2);
395+
396+
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
397+
398+
const R: f64 = 6_371_000.0; // mean Earth radius in meters
399+
R * c
400+
}
401+
318402
/// Calculate principal radii of curvature
319403
///
320404
/// The [principal radii of curvature](https://en.wikipedia.org/wiki/Earth_radius) are used to
@@ -728,6 +812,7 @@ pub fn magnetic_anomaly(
728812
mod tests {
729813
use super::*;
730814
use assert_approx_eq::assert_approx_eq;
815+
const KM: f64 = 1000.0;
731816
#[test]
732817
fn vector_to_skew_symmetric() {
733818
let v: Vector3<f64> = Vector3::new(1.0, 2.0, 3.0);
@@ -855,4 +940,70 @@ mod tests {
855940
assert_approx_eq!(rot1[(2, 1)], rot2[(2, 1)], 1e-7);
856941
assert_approx_eq!(rot1[(2, 2)], rot2[(2, 2)], 1e-7);
857942
}
943+
#[test]
944+
fn test_meters_ned_to_dlat_dlon() {
945+
// Test the conversion from North/East meters to latitude/longitude deltas
946+
947+
// At the equator, 1 degree of latitude is approximately 111.32 km
948+
// and 1 degree of longitude is approximately 111.32 km as well
949+
let lat_rad = 0.0; // equator
950+
let alt_m = 0.0; // sea level
951+
952+
// Test northward offset
953+
let (dlat, dlon) = meters_ned_to_dlat_dlon(lat_rad, alt_m, 1852.0, 0.0);
954+
assert_approx_eq!(dlat.abs(), (1.0 / 60.0 as f64).to_radians(), 0.0001); // ~1 degree latitude change
955+
assert_approx_eq!(dlon.abs(), 0.0, 0.0001); // ~0 longitude change
956+
957+
// Test eastward offset
958+
let (dlat, dlon) = meters_ned_to_dlat_dlon(lat_rad, alt_m, 0.0, 1852.0);
959+
assert!(dlat.abs() < 0.0001); // ~0 latitude change
960+
assert_approx_eq!(dlon.abs(), (1.0 / 60.0 as f64).to_radians(), 0.0001); // ~1 degree longitude change
961+
962+
// Test at 45 degrees north, where longitude degrees are shorter
963+
let lat_rad = 45.0_f64.to_radians();
964+
965+
// At 45°N, 1 degree of longitude is about 111.32 * cos(45°) = 78.7 km
966+
let (dlat, dlon) = meters_ned_to_dlat_dlon(lat_rad, alt_m, 0.0, 111.320);
967+
assert_approx_eq!(dlat.abs(), 0.0, 0.0001); // ~0 latitude change
968+
assert_approx_eq!(dlon.abs(), 0.0, 0.0001); // ~1 degree longitude change
969+
}
970+
971+
#[test]
972+
fn zero_distance() {
973+
let lat = 40.0_f64.to_radians();
974+
let lon = -75.0_f64.to_radians();
975+
let d = haversine_distance(lat, lon, lat, lon);
976+
assert!(d.abs() < 1e-9, "Zero distance should be ~0, got {}", d);
977+
}
978+
979+
#[test]
980+
fn quarter_circumference_equator() {
981+
// From (0,0) to (0,90°E) is a quarter of Earth's circumference
982+
let d = haversine_distance(0.0, 0.0, 0.0, 90_f64.to_radians());
983+
let expected = std::f64::consts::FRAC_PI_2 * 6_371_000.0; // R * π/2
984+
let err = (d - expected).abs();
985+
assert!(err < 1e-6 * expected, "Error too large: got {}, expected {}", d, expected);
986+
}
987+
988+
#[test]
989+
fn antipodal_points() {
990+
// (0,0) vs (0,180°E) → half Earth's circumference
991+
let d = haversine_distance(0.0, 0.0, 0.0, 180_f64.to_radians());
992+
let expected = std::f64::consts::PI * 6_371_000.0; // R * π
993+
let err = (d - expected).abs();
994+
assert!(err < 1e-6 * expected, "Error too large: got {}, expected {}", d, expected);
995+
}
996+
997+
#[test]
998+
fn known_baseline_nyc_to_london() {
999+
// Approx coordinates
1000+
let nyc_lat = 40.7128_f64.to_radians();
1001+
let nyc_lon = -74.0060_f64.to_radians();
1002+
let lon_lat = 51.5074_f64.to_radians();
1003+
let lon_lon = -0.1278_f64.to_radians();
1004+
1005+
let d = haversine_distance(nyc_lat, nyc_lon, lon_lat, lon_lon);
1006+
// Real-world great-circle distance ~5567 km
1007+
assert!((d / KM - 5567.0).abs() < 50.0, "NYC-LON distance off: {} km", d / KM);
1008+
}
8581009
}

0 commit comments

Comments
 (0)