Satellite pass prediction and contact window computation
0
fork

Configure Feed

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

contact: split test.ml into test_contact.ml + runner (E600/E605)

Move the test functions into a Test_contact module exposing a [suite]
value, and reduce test.ml to the single Alcotest.run call. This fixes
both "missing test_contact.ml" (E605) and the "Alcotest.run only in
test.ml" (E600) requirements.

+97 -97
+1 -97
test/test.ml
··· 1 - (** Tests for ocaml-contact. 2 - 3 - GMAT reference: station_contacts_report.txt from GMAT R2026a shows 14 4 - contact windows from LA (34.05 N, 241.75 E) over 3 days for an ISS-like 5 - orbit. Our predictions should find a similar number of passes with 6 - comparable timing. 7 - 8 - Source: GMAT R2026a, station_contacts.script. *) 9 - 10 - (* Same TLE used in the GMAT script *) 11 - let tle_string = 12 - "ISS (ZARYA)\n\ 13 - 1 25544U 98067A 26001.00000000 .00001764 00000+0 39538-4 0 9991\n\ 14 - 2 25544 51.6416 45.0000 0008003 90.0000 0.0000 15.49560000 15" 15 - 16 - let parse_tle () = 17 - match Sgp4.parse_tle_string tle_string with 18 - | Ok tle -> tle 19 - | Error e -> Alcotest.failf "TLE parse error: %a" Sgp4.pp_error e 20 - 21 - (* LA ground station (same as GMAT script) *) 22 - let la = Contact.ground_station ~lat:34.05 ~lon:(-118.25) ~alt:0.071 23 - 24 - let predict_passes () = 25 - let tle = parse_tle () in 26 - let passes = Contact.predict tle la ~duration_days:3.0 in 27 - Fmt.pr " Found %d passes over LA in 3 days\n" (List.length passes); 28 - List.iteri 29 - (fun i p -> 30 - Fmt.pr " %2d: %s max_el=%.1f deg dur=%.0fs\n" (i + 1) 31 - p.Contact.aos_epoch p.max_elevation p.duration) 32 - passes; 33 - (* GMAT found 14 passes. Our SGP4-based prediction may differ slightly 34 - (different force model, different elevation threshold) but should be 35 - in the same ballpark: 10-18 passes. *) 36 - Alcotest.(check bool) 37 - "reasonable pass count" true 38 - (List.length passes >= 8 && List.length passes <= 20) 39 - 40 - let pass_properties () = 41 - let tle = parse_tle () in 42 - let passes = Contact.predict tle la ~duration_days:3.0 in 43 - List.iter 44 - (fun p -> 45 - (* All passes should have positive duration *) 46 - Alcotest.(check bool) "positive duration" true (p.Contact.duration > 0.0); 47 - (* Max elevation should be above min_elevation (default 5 deg) *) 48 - Alcotest.(check bool) "max el >= 5 deg" true (p.max_elevation >= 5.0); 49 - (* Max elevation should be physical (< 90 deg) *) 50 - Alcotest.(check bool) "max el <= 90 deg" true (p.max_elevation <= 90.0); 51 - (* Duration should be physical (< 15 min for LEO) *) 52 - Alcotest.(check bool) "duration < 15 min" true (p.duration < 900.0); 53 - (* AOS before LOS *) 54 - Alcotest.(check bool) "AOS < LOS" true (p.aos_time < p.los_time)) 55 - passes 56 - 57 - let elevation () = 58 - let tle = parse_tle () in 59 - (* At epoch, satellite may or may not be visible *) 60 - let epoch_unix = Sgp4.epoch_unix tle in 61 - match Contact.elevation tle la epoch_unix with 62 - | None -> Alcotest.fail "elevation returned None" 63 - | Some el -> 64 - Fmt.pr " Elevation at epoch: %.1f deg\n" el; 65 - (* Should be a valid angle *) 66 - Alcotest.(check bool) "valid elevation" true (el >= -90.0 && el <= 90.0) 67 - 68 - let no_passes_wrong_incl () = 69 - (* An equatorial satellite (0 deg inclination) should never pass over 70 - LA at 34 deg latitude — or at most barely graze the horizon *) 71 - let eq_tle_str = 72 - "EQUATORIAL\n\ 73 - 1 99999U 26001A 26001.00000000 .00001764 00000+0 39538-4 0 9991\n\ 74 - 2 99999 0.0000 45.0000 0008003 90.0000 0.0000 15.49560000 15" 75 - in 76 - match Sgp4.parse_tle_string eq_tle_str with 77 - | Error _ -> () (* TLE may not parse — that's OK *) 78 - | Ok tle -> 79 - let passes = 80 - Contact.predict tle la ~duration_days:1.0 ~min_elevation:10.0 81 - in 82 - Fmt.pr " Equatorial sat passes over LA (>10 deg): %d\n" 83 - (List.length passes); 84 - (* Should find 0 or very few passes *) 85 - Alcotest.(check bool) "few/no passes" true (List.length passes <= 2) 86 - 87 - let () = 88 - Alcotest.run "contact" 89 - [ 90 - ( "pass-prediction", 91 - [ 92 - Alcotest.test_case "predict passes" `Quick predict_passes; 93 - Alcotest.test_case "pass properties" `Quick pass_properties; 94 - Alcotest.test_case "elevation" `Quick elevation; 95 - Alcotest.test_case "equatorial no passes" `Quick no_passes_wrong_incl; 96 - ] ); 97 - ] 1 + let () = Alcotest.run "contact" [ Test_contact.suite ]
+94
test/test_contact.ml
··· 1 + (** Tests for ocaml-contact. 2 + 3 + GMAT reference: station_contacts_report.txt from GMAT R2026a shows 14 4 + contact windows from LA (34.05 N, 241.75 E) over 3 days for an ISS-like 5 + orbit. Our predictions should find a similar number of passes with 6 + comparable timing. 7 + 8 + Source: GMAT R2026a, station_contacts.script. *) 9 + 10 + (* Same TLE used in the GMAT script *) 11 + let tle_string = 12 + "ISS (ZARYA)\n\ 13 + 1 25544U 98067A 26001.00000000 .00001764 00000+0 39538-4 0 9991\n\ 14 + 2 25544 51.6416 45.0000 0008003 90.0000 0.0000 15.49560000 15" 15 + 16 + let parse_tle () = 17 + match Sgp4.parse_tle_string tle_string with 18 + | Ok tle -> tle 19 + | Error e -> Alcotest.failf "TLE parse error: %a" Sgp4.pp_error e 20 + 21 + (* LA ground station (same as GMAT script) *) 22 + let la = Contact.ground_station ~lat:34.05 ~lon:(-118.25) ~alt:0.071 23 + 24 + let predict_passes () = 25 + let tle = parse_tle () in 26 + let passes = Contact.predict tle la ~duration_days:3.0 in 27 + Fmt.pr " Found %d passes over LA in 3 days\n" (List.length passes); 28 + List.iteri 29 + (fun i p -> 30 + Fmt.pr " %2d: %s max_el=%.1f deg dur=%.0fs\n" (i + 1) 31 + p.Contact.aos_epoch p.max_elevation p.duration) 32 + passes; 33 + (* GMAT found 14 passes. Our SGP4-based prediction may differ slightly 34 + (different force model, different elevation threshold) but should be 35 + in the same ballpark: 10-18 passes. *) 36 + Alcotest.(check bool) 37 + "reasonable pass count" true 38 + (List.length passes >= 8 && List.length passes <= 20) 39 + 40 + let pass_properties () = 41 + let tle = parse_tle () in 42 + let passes = Contact.predict tle la ~duration_days:3.0 in 43 + List.iter 44 + (fun p -> 45 + (* All passes should have positive duration *) 46 + Alcotest.(check bool) "positive duration" true (p.Contact.duration > 0.0); 47 + (* Max elevation should be above min_elevation (default 5 deg) *) 48 + Alcotest.(check bool) "max el >= 5 deg" true (p.max_elevation >= 5.0); 49 + (* Max elevation should be physical (< 90 deg) *) 50 + Alcotest.(check bool) "max el <= 90 deg" true (p.max_elevation <= 90.0); 51 + (* Duration should be physical (< 15 min for LEO) *) 52 + Alcotest.(check bool) "duration < 15 min" true (p.duration < 900.0); 53 + (* AOS before LOS *) 54 + Alcotest.(check bool) "AOS < LOS" true (p.aos_time < p.los_time)) 55 + passes 56 + 57 + let elevation () = 58 + let tle = parse_tle () in 59 + (* At epoch, satellite may or may not be visible *) 60 + let epoch_unix = Sgp4.epoch_unix tle in 61 + match Contact.elevation tle la epoch_unix with 62 + | None -> Alcotest.fail "elevation returned None" 63 + | Some el -> 64 + Fmt.pr " Elevation at epoch: %.1f deg\n" el; 65 + (* Should be a valid angle *) 66 + Alcotest.(check bool) "valid elevation" true (el >= -90.0 && el <= 90.0) 67 + 68 + let no_passes_wrong_incl () = 69 + (* An equatorial satellite (0 deg inclination) should never pass over 70 + LA at 34 deg latitude — or at most barely graze the horizon *) 71 + let eq_tle_str = 72 + "EQUATORIAL\n\ 73 + 1 99999U 26001A 26001.00000000 .00001764 00000+0 39538-4 0 9991\n\ 74 + 2 99999 0.0000 45.0000 0008003 90.0000 0.0000 15.49560000 15" 75 + in 76 + match Sgp4.parse_tle_string eq_tle_str with 77 + | Error _ -> () (* TLE may not parse — that's OK *) 78 + | Ok tle -> 79 + let passes = 80 + Contact.predict tle la ~duration_days:1.0 ~min_elevation:10.0 81 + in 82 + Fmt.pr " Equatorial sat passes over LA (>10 deg): %d\n" 83 + (List.length passes); 84 + (* Should find 0 or very few passes *) 85 + Alcotest.(check bool) "few/no passes" true (List.length passes <= 2) 86 + 87 + let suite = 88 + ( "contact", 89 + [ 90 + Alcotest.test_case "predict passes" `Quick predict_passes; 91 + Alcotest.test_case "pass properties" `Quick pass_properties; 92 + Alcotest.test_case "elevation" `Quick elevation; 93 + Alcotest.test_case "equatorial no passes" `Quick no_passes_wrong_incl; 94 + ] )
+2
test/test_contact.mli
··· 1 + val suite : string * unit Alcotest.test_case list 2 + (** Test suite. *)