Orbit Data Messages (CCSDS 502.0-B-3)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Split ocaml-transport into ocaml-cltu and ocaml-cop1

- ocaml-cltu: CLTU encoding/decoding (CCSDS 231.0-B) with BCH(63,56),
ASM sync markers (131.0-B), and stream sync parsers
- ocaml-cop1: COP-1 state machines (CCSDS 232.1-B) for FOP-1 (ground)
and FARM-1 (flight) with Eio service layer
- cltu-eio: CLTU/ASM send/recv over Eio flows
- cop1-eio: COP-1 service layer with Eio timer management

+95 -11
+95 -11
test/interop/gmat/test.ml
··· 148 148 (rel *. 100.0) sv.epoch) 149 149 svs 150 150 151 - (* Interpolation: value at a known epoch should match the data point. *) 152 - let test_leo_interpolation_at_epoch () = 151 + (* MISSION-READY TEST: Interpolation accuracy. 152 + GMAT uses Lagrange order 7; we use linear interpolation. 153 + At the midpoint between two 60s-spaced LEO data points, linear interpolation 154 + should match within ~0.1 km (LEO curvature over 30s). 155 + If this fails, our interpolation is not mission-grade. *) 156 + let test_leo_interpolation_midpoint () = 153 157 let oem = parse_oem "leo_7day.oem" in 154 158 let seg = List.hd (Odm.segments oem) in 155 159 let svs = Odm.state_vectors seg in 156 - (* Pick a data point in the middle and interpolate at its exact epoch *) 157 - let mid = svs.(5000) in 158 - match Odm.interpolate seg with 159 - | exception _ -> Alcotest.fail "interpolate raised exception" 160 - | _interp -> 161 - (* Just verify interpolation function exists and doesn't crash on this OEM *) 162 - () 160 + (* Use vectors 5000 and 5002 (120s apart). The "truth" at the midpoint 161 + is vector 5001 (GMAT propagated, not interpolated). Our interpolation 162 + between 5000 and 5002 should approximate 5001. *) 163 + let epoch_to_unix e = 164 + match Ptime.of_rfc3339 (e ^ "Z") with 165 + | Ok (t, _, _) -> Ptime.to_float_s t 166 + | Error _ -> Alcotest.failf "bad epoch: %s" e 167 + in 168 + let t0 = epoch_to_unix svs.(5000).epoch in 169 + let t2 = epoch_to_unix svs.(5002).epoch in 170 + let t_mid = (t0 +. t2) /. 2.0 in 171 + match Odm.interpolate seg t_mid with 172 + | None -> Alcotest.fail "interpolation returned None" 173 + | Some interp -> 174 + let truth = svs.(5001).Odm.pos in 175 + let dx = interp.x -. truth.x in 176 + let dy = interp.y -. truth.y in 177 + let dz = interp.z -. truth.z in 178 + let err = Float.sqrt ((dx *. dx) +. (dy *. dy) +. (dz *. dz)) in 179 + (* Linear interpolation over 120s LEO arc: expect ~0.01-0.1 km error. 180 + Mission-grade (Lagrange 7) would be < 1e-6 km. 181 + This test documents the actual error — if it exceeds 1 km, something 182 + is very wrong. If it's > 0.01 km, we know linear is insufficient. *) 183 + Printf.printf 184 + " LEO interpolation error (linear, 120s arc): %.6f km (%.1f m)\n" err 185 + (err *. 1000.0); 186 + if err > 1.0 then Alcotest.failf "interpolation error > 1 km: %.6f km" err; 187 + (* Flag if error is > 10 m — this means linear interpolation is not 188 + mission-grade for LEO at 60s step *) 189 + if err > 0.01 then 190 + Printf.printf 191 + " WARNING: linear interpolation error > 10 m — not mission-grade\n" 192 + 193 + (* MISSION-READY TEST: Interpolation on Molniya near perigee. 194 + High eccentricity + fast perigee passage = worst case for linear interpolation. 195 + At 30s step near perigee (~10 km/s), curvature is extreme. 196 + This SHOULD show large interpolation errors. *) 197 + let test_molniya_interpolation_perigee () = 198 + let oem = parse_oem "molniya_2day.oem" in 199 + let seg = List.hd (Odm.segments oem) in 200 + let svs = Odm.state_vectors seg in 201 + (* Find a perigee passage (minimum radius) *) 202 + let min_i = ref 0 in 203 + let min_r = ref Float.infinity in 204 + Array.iteri 205 + (fun i sv -> 206 + let r = vec3_norm sv.Odm.pos in 207 + if r < !min_r then ( 208 + min_r := r; 209 + min_i := i)) 210 + svs; 211 + (* Interpolate between the points bracketing perigee *) 212 + let i = !min_i in 213 + if i < 2 || i >= Array.length svs - 2 then 214 + Alcotest.fail "perigee too close to edge" 215 + else 216 + let epoch_to_unix e = 217 + match Ptime.of_rfc3339 (e ^ "Z") with 218 + | Ok (t, _, _) -> Ptime.to_float_s t 219 + | Error _ -> Alcotest.failf "bad epoch: %s" e 220 + in 221 + let t0 = epoch_to_unix svs.(i - 1).epoch in 222 + let t2 = epoch_to_unix svs.(i + 1).epoch in 223 + let t_mid = (t0 +. t2) /. 2.0 in 224 + match Odm.interpolate seg t_mid with 225 + | None -> Alcotest.fail "interpolation returned None at perigee" 226 + | Some interp -> 227 + let truth = svs.(i).Odm.pos in 228 + let dx = interp.x -. truth.x in 229 + let dy = interp.y -. truth.y in 230 + let dz = interp.z -. truth.z in 231 + let err = Float.sqrt ((dx *. dx) +. (dy *. dy) +. (dz *. dz)) in 232 + Printf.printf 233 + " Molniya perigee interpolation error (linear, 60s arc): %.6f km \ 234 + (%.1f m)\n" 235 + err (err *. 1000.0); 236 + Printf.printf " Perigee radius: %.1f km, velocity: ~%.1f km/s\n" !min_r 237 + (vec3_norm svs.(i).vel); 238 + if err > 10.0 then 239 + Alcotest.failf 240 + "interpolation error > 10 km at Molniya perigee: %.3f km" err; 241 + if err > 0.1 then 242 + Printf.printf 243 + " WARNING: linear interpolation error > 100 m at perigee — not \ 244 + mission-grade\n" 163 245 164 246 (* --- GEO 3-day --- 165 247 Source: GMAT R2026a, geo_3day.script. ··· 359 441 Alcotest.test_case "epoch monotonic" `Quick test_leo_epoch_monotonic; 360 442 Alcotest.test_case "energy conservation" `Quick 361 443 test_leo_energy_conservation; 362 - Alcotest.test_case "interpolation" `Quick 363 - test_leo_interpolation_at_epoch; 444 + Alcotest.test_case "interpolation midpoint" `Quick 445 + test_leo_interpolation_midpoint; 364 446 ] ); 365 447 ( "geo", 366 448 [ ··· 381 463 Alcotest.test_case "vis-viva consistency" `Quick test_molniya_vis_viva; 382 464 Alcotest.test_case "epoch monotonic" `Quick 383 465 test_molniya_epoch_monotonic; 466 + Alcotest.test_case "interpolation at perigee" `Quick 467 + test_molniya_interpolation_perigee; 384 468 ] ); 385 469 ]