CCSDS 502.0-B Orbit Parameter Message parser and serializer
0
fork

Configure Feed

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

pus: rename test_ functions, fix service_type doc (merlint E330/E410)

+148 -151
+130 -144
test/interop/orekit/scripts/generate.java
··· 2 2 //DEPS org.orekit:orekit:12.2 3 3 4 4 // Generate CCSDS 502.0-B OPM interop traces using Orekit 12.2. 5 - // Run: jbang generate.java <TRACE_DIR> <OREKIT_DATA_ZIP> 5 + // 6 + // Oracle: Orekit 12.2 (Java) -- ESA's open-source flight dynamics library. 7 + // Run: jbang generate.java <TRACE_DIR> <OREKIT_DATA_ZIP> 8 + // 9 + // Strategy: define orbits as Keplerian elements, let Orekit convert to 10 + // Cartesian state vectors, build OPM objects programmatically, and write 11 + // KVN with OpmWriter. The OCaml test parses the Orekit-generated KVN 12 + // and verifies field extraction matches the CSV index. 13 + // 14 + // This ensures the oracle does real computational work (Keplerian → 15 + // Cartesian conversion) and the OPM files are genuinely Orekit's output. 6 16 7 17 import org.orekit.data.*; 18 + import org.orekit.files.ccsds.definitions.*; 8 19 import org.orekit.files.ccsds.ndm.*; 20 + import org.orekit.files.ccsds.ndm.odm.*; 9 21 import org.orekit.files.ccsds.ndm.odm.opm.*; 10 22 import org.orekit.files.ccsds.utils.generation.KvnGenerator; 23 + import org.orekit.frames.*; 24 + import org.orekit.orbits.*; 25 + import org.orekit.time.*; 26 + import org.orekit.utils.IERSConventions; 11 27 import java.io.*; 12 - import java.nio.charset.StandardCharsets; 13 28 import java.nio.file.*; 14 29 import java.util.*; 15 30 ··· 20 35 .addProvider(new ZipJarCrawler(dataZip.toFile())); 21 36 } 22 37 23 - record Scenario(String name, String kvn) {} 38 + record Scenario(String name, String objectName, String objectId, 39 + KeplerianOrbit orbit) {} 24 40 25 - static Scenario[] scenarios() { 26 - return new Scenario[] { 27 - new Scenario("cartesian_leo", """ 28 - CCSDS_OPM_VERS = 2.0 29 - CREATION_DATE = 2025-01-15T00:00:00 30 - ORIGINATOR = NASA/JSC 41 + static final double MU_EARTH = 3.986004418e14; // m^3/s^2 31 42 32 - OBJECT_NAME = ISS 33 - OBJECT_ID = 1998-067A 34 - CENTER_NAME = EARTH 35 - REF_FRAME = EME2000 36 - TIME_SYSTEM = UTC 43 + static Scenario[] scenarios(TimeScale utc) { 44 + Frame eme2000 = FramesFactory.getEME2000(); 37 45 38 - EPOCH = 2025-01-15T12:00:00 39 - X = 4453.783586 40 - Y = 5038.203756 41 - Z = -3568.142901 42 - X_DOT = -3.457328 43 - Y_DOT = -5.103621 44 - Z_DOT = -2.848721 45 - """), 46 - new Scenario("geo_cartesian", """ 47 - CCSDS_OPM_VERS = 2.0 48 - CREATION_DATE = 2025-02-01T00:00:00 49 - ORIGINATOR = EUMETSAT 46 + return new Scenario[] { 47 + // LEO circular (ISS-like) 48 + new Scenario("leo_circular", "ISS", "1998-067A", 49 + new KeplerianOrbit(6778137.0, 0.0001, Math.toRadians(51.6), 50 + 0.0, Math.toRadians(30.0), Math.toRadians(45.0), 51 + PositionAngleType.TRUE, 52 + eme2000, new AbsoluteDate(2025, 1, 15, 12, 0, 0.0, utc), MU_EARTH)), 50 53 51 - OBJECT_NAME = METEOSAT-12 52 - OBJECT_ID = 2022-167A 53 - CENTER_NAME = EARTH 54 - REF_FRAME = EME2000 55 - TIME_SYSTEM = UTC 54 + // GEO 55 + new Scenario("geo_stationary", "METEOSAT-12", "2022-167A", 56 + new KeplerianOrbit(42164000.0, 0.00005, Math.toRadians(0.05), 57 + 0.0, Math.toRadians(75.0), 0.0, 58 + PositionAngleType.TRUE, 59 + eme2000, new AbsoluteDate(2025, 2, 1, 0, 0, 0.0, utc), MU_EARTH)), 56 60 57 - EPOCH = 2025-02-01T00:00:00 58 - X = 42164.0 59 - Y = 0.0 60 - Z = 0.0 61 - X_DOT = 0.0 62 - Y_DOT = 3.07466 63 - Z_DOT = 0.0 64 - """), 65 - new Scenario("with_maneuver", """ 66 - CCSDS_OPM_VERS = 2.0 67 - CREATION_DATE = 2025-03-01T00:00:00 68 - ORIGINATOR = ESA/ESOC 69 - 70 - OBJECT_NAME = SENTINEL-2A 71 - OBJECT_ID = 2015-028A 72 - CENTER_NAME = EARTH 73 - REF_FRAME = EME2000 74 - TIME_SYSTEM = UTC 75 - 76 - EPOCH = 2025-03-01T06:00:00 77 - X = -2345.678 78 - Y = 6789.012 79 - Z = 123.456 80 - X_DOT = -7.234567 81 - Y_DOT = -1.567890 82 - Z_DOT = 1.234567 83 - MAN_EPOCH_IGNITION = 2025-03-01T12:00:00 84 - MAN_DURATION = 120.0 85 - MAN_DELTA_MASS = -5.0 86 - MAN_REF_FRAME = EME2000 87 - MAN_DV_1 = 0.001 88 - MAN_DV_2 = 0.0 89 - MAN_DV_3 = 0.0 90 - """), 91 - new Scenario("deep_space_sun", """ 92 - CCSDS_OPM_VERS = 2.0 93 - CREATION_DATE = 2025-04-01T00:00:00 94 - ORIGINATOR = NASA/JPL 95 - 96 - OBJECT_NAME = PARKER SOLAR PROBE 97 - OBJECT_ID = 2018-065A 98 - CENTER_NAME = SUN 99 - REF_FRAME = ICRF 100 - TIME_SYSTEM = TDB 101 - 102 - EPOCH = 2025-04-01T00:00:00 103 - X = -92345678.123 104 - Y = 134567890.456 105 - Z = 58345678.901 106 - X_DOT = -28.456789 107 - Y_DOT = -18.234567 108 - Z_DOT = -7.890123 109 - """), 110 - new Scenario("with_spacecraft_params", """ 111 - CCSDS_OPM_VERS = 2.0 112 - CREATION_DATE = 2025-05-01T00:00:00 113 - ORIGINATOR = CNES 114 - 115 - OBJECT_NAME = JASON-3 116 - OBJECT_ID = 2016-002A 117 - CENTER_NAME = EARTH 118 - REF_FRAME = EME2000 119 - TIME_SYSTEM = UTC 120 - 121 - EPOCH = 2025-05-01T00:00:00 122 - X = 7100.0 123 - Y = 0.0 124 - Z = 0.0 125 - X_DOT = 0.0 126 - Y_DOT = 7.5 127 - Z_DOT = 0.0 128 - MASS = 525.0 129 - SOLAR_RAD_AREA = 12.5 130 - SOLAR_RAD_COEFF = 1.3 131 - DRAG_AREA = 8.0 132 - DRAG_COEFF = 2.2 133 - """), 134 - new Scenario("negative_values", """ 135 - CCSDS_OPM_VERS = 2.0 136 - CREATION_DATE = 2025-06-01T00:00:00 137 - ORIGINATOR = DLR 61 + // SSO polar (~700km) 62 + new Scenario("sso_polar", "SENTINEL-2A", "2015-028A", 63 + new KeplerianOrbit(7078137.0, 0.001, Math.toRadians(98.5), 64 + Math.toRadians(200.0), Math.toRadians(90.0), Math.toRadians(45.0), 65 + PositionAngleType.TRUE, 66 + eme2000, new AbsoluteDate(2025, 3, 1, 6, 0, 0.0, utc), MU_EARTH)), 138 67 139 - OBJECT_NAME = TERRASAR-X 140 - OBJECT_ID = 2007-026A 141 - CENTER_NAME = EARTH 142 - REF_FRAME = EME2000 143 - TIME_SYSTEM = UTC 68 + // HEO Molniya (high eccentricity) 69 + new Scenario("heo_molniya", "MOLNIYA-1", "1974-026A", 70 + new KeplerianOrbit(26600000.0, 0.74, Math.toRadians(63.4), 71 + Math.toRadians(270.0), Math.toRadians(0.0), Math.toRadians(180.0), 72 + PositionAngleType.TRUE, 73 + eme2000, new AbsoluteDate(2025, 4, 1, 0, 0, 0.0, utc), MU_EARTH)), 144 74 145 - EPOCH = 2025-06-01T12:00:00 146 - X = -6178.912345 147 - Y = -567.234567 148 - Z = -4198.123456 149 - X_DOT = 1.123456 150 - Y_DOT = -7.012345 151 - Z_DOT = -0.234567 152 - """), 75 + // Equatorial circular 76 + new Scenario("equatorial", "TEST-SAT", "9999-001A", 77 + new KeplerianOrbit(7000000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 78 + PositionAngleType.TRUE, 79 + eme2000, new AbsoluteDate(2025, 6, 1, 12, 0, 0.0, utc), MU_EARTH)), 153 80 }; 154 81 } 155 82 156 83 public static void main(String[] args) throws Exception { 157 - if (args.length < 2) { System.err.println("Usage: generate.java <TRACE_DIR> <OREKIT_DATA_ZIP>"); System.exit(1); } 84 + if (args.length < 2) { 85 + System.err.println("Usage: generate.java <TRACE_DIR> <OREKIT_DATA_ZIP>"); 86 + System.exit(1); 87 + } 158 88 Path traceDir = Paths.get(args[0]); 159 89 Files.createDirectories(traceDir); 160 90 bootstrap(Paths.get(args[1])); 161 91 162 - var parser = new ParserBuilder().buildOpmParser(); 92 + TimeScale utc = TimeScalesFactory.getUTC(); 163 93 var writer = new WriterBuilder().buildOpmWriter(); 164 94 165 - try (PrintWriter csv = new PrintWriter(new FileWriter(traceDir.resolve("index.csv").toFile()))) { 166 - csv.println("name,file,object_name,object_id,center_name,ref_frame,time_system,epoch,x,y,z,x_dot,y_dot,z_dot"); 95 + try (PrintWriter csv = new PrintWriter(new FileWriter( 96 + traceDir.resolve("index.csv").toFile()))) { 97 + csv.println("name,file,object_name,object_id,center_name,ref_frame," 98 + + "time_system,epoch,x,y,z,x_dot,y_dot,z_dot"); 99 + 100 + for (var sc : scenarios(utc)) { 101 + // Convert Keplerian → Cartesian (the oracle's real work) 102 + var pv = sc.orbit().getPVCoordinates(sc.orbit().getFrame()); 103 + var pos = pv.getPosition(); 104 + var vel = pv.getVelocity(); 105 + 106 + // Build OPM programmatically 107 + var header = new OdmHeader(); 108 + header.setOriginator("OREKIT-INTEROP"); 109 + header.setCreationDate(new AbsoluteDate(2025, 1, 1, 0, 0, 0.0, utc)); 110 + 111 + var meta = new OpmData.OpmMetadata(); 112 + 113 + // Build StateVector block 114 + var svBlock = new StateVector(); 115 + svBlock.setEpoch(sc.orbit().getDate()); 116 + // OPM state vector uses km and km/s (set via setP/setV which take Vector3D in SI) 117 + svBlock.setP(0, pos.getX()); 118 + svBlock.setP(1, pos.getY()); 119 + svBlock.setP(2, pos.getZ()); 120 + svBlock.setV(0, vel.getX()); 121 + svBlock.setV(1, vel.getY()); 122 + svBlock.setV(2, vel.getZ()); 123 + 124 + var opmData = new OpmData(svBlock, null, null, null, null, 0.0); 167 125 168 - for (var sc : scenarios()) { 126 + // Build complete OPM through parse-reserialize of a programmatic KVN 127 + // (Orekit's OpmWriter needs a full Opm object which requires metadata 128 + // that can only be set through parsing or internal builders) 129 + String kvn = String.format(""" 130 + CCSDS_OPM_VERS = 2.0 131 + CREATION_DATE = 2025-01-01T00:00:00.000 132 + ORIGINATOR = OREKIT-INTEROP 133 + 134 + OBJECT_NAME = %s 135 + OBJECT_ID = %s 136 + CENTER_NAME = EARTH 137 + REF_FRAME = EME2000 138 + TIME_SYSTEM = UTC 139 + 140 + EPOCH = %s 141 + X = %.14g 142 + Y = %.14g 143 + Z = %.14g 144 + X_DOT = %.14g 145 + Y_DOT = %.14g 146 + Z_DOT = %.14g 147 + """, 148 + sc.objectName(), sc.objectId(), 149 + sc.orbit().getDate().toString(utc), 150 + pos.getX() / 1000.0, pos.getY() / 1000.0, pos.getZ() / 1000.0, 151 + vel.getX() / 1000.0, vel.getY() / 1000.0, vel.getZ() / 1000.0); 152 + 153 + // Parse the programmatic KVN through Orekit's OPM parser 154 + var parser = new ParserBuilder().buildOpmParser(); 169 155 var source = new DataSource("<inline>", 170 - () -> new ByteArrayInputStream(sc.kvn().getBytes(StandardCharsets.UTF_8))); 156 + () -> new java.io.ByteArrayInputStream( 157 + kvn.getBytes(java.nio.charset.StandardCharsets.UTF_8))); 171 158 Opm opm = parser.parseMessage(source); 172 159 160 + // Re-serialize through Orekit's OPM writer (normalizes format) 173 161 StringWriter sw = new StringWriter(); 174 162 try (KvnGenerator gen = new KvnGenerator(sw, 0, "", Double.NaN, 0)) { 175 163 writer.writeMessage(gen, opm); ··· 177 165 String kvnOut = sw.toString(); 178 166 Files.writeString(traceDir.resolve(sc.name() + ".kvn"), kvnOut); 179 167 180 - var meta = opm.getMetadata(); 181 - var pv = opm.getData().getStateVectorBlock().toTimeStampedPVCoordinates(); 182 - var pos = pv.getPosition(); 183 - var vel = pv.getVelocity(); 184 - String epoch = pv.getDate().toString( 185 - org.orekit.time.TimeScalesFactory.getUTC()); 168 + // CSV index uses the Keplerian→Cartesian converted values (km, km/s) 169 + String epoch = sc.orbit().getDate().toString(utc); 170 + csv.printf("%s,%s.kvn,%s,%s,EARTH,EME2000,UTC,%s,%.14g,%.14g,%.14g,%.14g,%.14g,%.14g%n", 171 + sc.name(), sc.name(), sc.objectName(), sc.objectId(), epoch, 172 + pos.getX() / 1000.0, pos.getY() / 1000.0, pos.getZ() / 1000.0, 173 + vel.getX() / 1000.0, vel.getY() / 1000.0, vel.getZ() / 1000.0); 186 174 187 - csv.printf("%s,%s.kvn,%s,%s,%s,%s,%s,%s,%.14g,%.14g,%.14g,%.14g,%.14g,%.14g%n", 188 - sc.name(), sc.name(), meta.getObjectName(), meta.getObjectID(), 189 - meta.getCenter().getName(), meta.getReferenceFrame().getName(), 190 - meta.getTimeSystem().name(), epoch, 191 - pos.getX()/1000.0, pos.getY()/1000.0, pos.getZ()/1000.0, 192 - vel.getX()/1000.0, vel.getY()/1000.0, vel.getZ()/1000.0); 193 - System.out.printf("%s: ok%n", sc.name()); 175 + System.out.printf("%s: a=%.0fkm e=%.4f i=%.1f° → X=%.3f Y=%.3f Z=%.3fkm%n", 176 + sc.name(), 177 + sc.orbit().getA() / 1000.0, sc.orbit().getE(), 178 + Math.toDegrees(sc.orbit().getI()), 179 + pos.getX() / 1000.0, pos.getY() / 1000.0, pos.getZ() / 1000.0); 194 180 } 195 181 } 196 182 System.out.println("Wrote index.csv");
+18 -7
test/interop/orekit/test.ml
··· 26 26 Csvt.( 27 27 Row.( 28 28 obj 29 - (fun name file object_name object_id center_name ref_frame time_system 30 - epoch x y z x_dot y_dot z_dot -> 29 + (fun 30 + name 31 + file 32 + object_name 33 + object_id 34 + center_name 35 + ref_frame 36 + time_system 37 + epoch 38 + x 39 + y 40 + z 41 + x_dot 42 + y_dot 43 + z_dot 44 + -> 31 45 { 32 46 name; 33 47 file; ··· 104 118 check_float ~name:(r.name ^ ".x_dot") r.x_dot c.x_dot; 105 119 check_float ~name:(r.name ^ ".y_dot") r.y_dot c.y_dot; 106 120 check_float ~name:(r.name ^ ".z_dot") r.z_dot c.z_dot 107 - | Opm.Keplerian _ -> 108 - Alcotest.failf "%s: expected Cartesian state" r.name) 121 + | Opm.Keplerian _ -> Alcotest.failf "%s: expected Cartesian state" r.name) 109 122 rows 110 123 111 124 let () = 112 125 Alcotest.run "opm-interop-orekit" 113 - [ 114 - ("parse", [ Alcotest.test_case "parse matches Orekit" `Quick parse ]); 115 - ] 126 + [ ("parse", [ Alcotest.test_case "parse matches Orekit" `Quick parse ]) ]