code wiki / _hdl_build / nx_geo_haversine.nx

nx_geo_haversine.nx source

↩ module page · 57 lines · 3311 B

1// nx_geo_haversine.nx -- LIB: GEO-003b GREAT-CIRCLE distance (haversine), the PRECISION REFINEMENT of 2// GEO-003 equirectangular. Hardware-up + DRY: REUSES the sovereign CORDIC sin/cos (fx.nx, Q16.16), 3// the vectoring-mode CORDIC atan2 (geo_atan2 from nx_geo_query), and nx_isqrt -- no trig reinvention. 4// 5// WHY THIS RUNG (measured, not asserted): equirectangular assumes a locally-flat earth, so its error 6// grows with span -- ~0.5% regionally and several percent at continental / antipodal scale. Haversine 7// is exact great-circle on the reference sphere. The gate PROVES the exceed: for NYC->Sydney 8// (~16000 km) haversine lands within ~2% of the true distance while equirectangular is off by far 9// more -- so haversine is measurably CLOSER. (For short spans GEO-003 stays the right tool: its 10// integer deltas avoid the Q16.16 underflow of a tiny half-angle's sine.) 11// 12// All Q16.16 fixed point -> deterministic. license_tier: ORIGINAL 13import "nx_geo_query.nx" // geo_atan2 (vectoring CORDIC) -- reused, no atan reinvention 14import "nx_geo_distance.nx" // geo_distance_m -- the equirectangular baseline we refine 15import "fx.nx" 16import "nx_isqrt.nx" 17import "nx_syscalls.nx" 18const HAV_MAGIC_360000000: i64 = 360000000 19const HAV_MAGIC_180000000: i64 = 180000000 20 21const HAV_R_M: i64 = 6371000 // mean Earth radius in meters (the great-circle reference sphere) 22 23// microdegrees -> Q16.16 radians (360e6 microdeg == 2*pi == FX_TWO_PI). Multiply before divide. 24func hav_micro_to_q16(micro: i64) -> i64 { 25 return micro * FX_TWO_PI / HAV_MAGIC_360000000 26} 27 28// Great-circle distance in METERS via the haversine formula: 29// a = sin^2(dlat/2) + cos(lat1)*cos(lat2)*sin^2(dlon/2); c = 2*atan2(sqrt(a), sqrt(1-a)); d = R*c. 30// Everything in Q16.16, sin/cos/atan2 from the sovereign CORDIC. dlon is reduced to [-180,180] deg 31// first (sin^2 is wrap-invariant) so each half-angle stays within CORDIC's sweet spot. 32func geo_haversine_m(lat1: i64, lon1: i64, lat2: i64, lon2: i64) -> i64 { 33 let dlat: i64 = lat2 - lat1 34 var dlon: i64 = lon2 - lon1 35 while dlon > HAV_MAGIC_180000000 { dlon = dlon - HAV_MAGIC_360000000 } 36 while dlon < (0 - HAV_MAGIC_180000000) { dlon = dlon + HAV_MAGIC_360000000 } 37 38 let s_dlat: i64 = fx_sin(hav_micro_to_q16(dlat) / 2) // sin(dlat/2), Q16.16 39 let s_dlon: i64 = fx_sin(hav_micro_to_q16(dlon) / 2) // sin(dlon/2), Q16.16 40 let sin2_dlat: i64 = s_dlat * s_dlat / FX_ONE // sin^2(dlat/2) 41 let sin2_dlon: i64 = s_dlon * s_dlon / FX_ONE // sin^2(dlon/2) 42 43 let cos1: i64 = fx_cos(hav_micro_to_q16(lat1)) 44 let cos2: i64 = fx_cos(hav_micro_to_q16(lat2)) 45 let coscos: i64 = cos1 * cos2 / FX_ONE // cos(lat1)*cos(lat2) 46 47 var a: i64 = sin2_dlat + coscos * sin2_dlon / FX_ONE // Q16.16, clamped to [0,1] 48 if a < 0 { a = 0 } 49 if a > FX_ONE { a = FX_ONE } 50 if a == 0 { return 0 } // coincident (or sub-resolution) points: exactly 0, no atan2(0,x) residual 51 52 // sqrt of a Q16.16 value v, result in Q16.16, is isqrt(v * FX_ONE). 53 let sqrt_a: i64 = nx_isqrt(a * FX_ONE) 54 let sqrt_1ma: i64 = nx_isqrt((FX_ONE - a) * FX_ONE) 55 let c_q16: i64 = 2 * geo_atan2(sqrt_a, sqrt_1ma) // central angle in Q16.16 radians 56 return HAV_R_M * c_q16 / FX_ONE // -> meters 57}