CCSDS 503.0-B Tracking Data Message parser and serializer
0
fork

Configure Feed

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

irmin: proof is a heap

+583 -338
+17
test/interop/orekit/dune
··· 1 + (test 2 + (name test) 3 + (libraries tdm alcotest csvt) 4 + (deps 5 + (source_tree traces) 6 + (source_tree scripts))) 7 + 8 + ; Regenerate traces against OREKIT: dune build @regen-traces 9 + 10 + (rule 11 + (alias regen-traces) 12 + (deps 13 + (source_tree scripts)) 14 + (action 15 + (chdir 16 + scripts 17 + (run bash ./generate.sh))))
+402 -337
test/interop/orekit/scripts/generate.java
··· 6 6 // Oracle: OREKIT 12.2 (Java) -- ESA's open-source flight dynamics library. 7 7 // Run: jbang generate.java <TRACE_DIR> 8 8 // 9 - // OREKIT needs orekit-data for time scale initialisation. This script 10 - // downloads the data zip automatically on first run and caches it next 11 - // to the script. 9 + // Strategy: build TDM KVN strings, parse with OREKIT TdmParser (validates 10 + // CCSDS compliance), then re-serialize with TdmWriter + KvnGenerator. 11 + // The re-serialized output is the trace our OCaml test reads. 12 + // 13 + // OREKIT needs orekit-data for time scales. We bootstrap a minimal dataset 14 + // (UTC-TAI.history) in a temp directory. 12 15 13 16 import org.orekit.data.DataContext; 14 17 import org.orekit.data.DataProvidersManager; 18 + import org.orekit.data.DataSource; 15 19 import org.orekit.data.DirectoryCrawler; 16 - import org.orekit.data.ZipJarCrawler; 17 - import org.orekit.files.ccsds.definitions.TimeSystem; 18 - import org.orekit.files.ccsds.ndm.tdm.ObservationType; 20 + import org.orekit.files.ccsds.ndm.ParserBuilder; 21 + import org.orekit.files.ccsds.ndm.WriterBuilder; 22 + import org.orekit.files.ccsds.ndm.tdm.Observation; 19 23 import org.orekit.files.ccsds.ndm.tdm.ObservationsBlock; 20 - import org.orekit.files.ccsds.ndm.tdm.RangeMode; 21 - import org.orekit.files.ccsds.ndm.tdm.RangeUnits; 22 - import org.orekit.files.ccsds.ndm.tdm.TdmFile; 23 - import org.orekit.files.ccsds.ndm.tdm.TdmHeader; 24 + import org.orekit.files.ccsds.ndm.tdm.ObservationType; 25 + import org.orekit.files.ccsds.ndm.tdm.Tdm; 24 26 import org.orekit.files.ccsds.ndm.tdm.TdmMetadata; 27 + import org.orekit.files.ccsds.ndm.tdm.TdmParser; 25 28 import org.orekit.files.ccsds.ndm.tdm.TdmWriter; 26 - import org.orekit.files.ccsds.ndm.tdm.AngleType; 27 - import org.orekit.files.ccsds.ndm.tdm.IntegrationReference; 28 - import org.orekit.files.ccsds.ndm.tdm.TimetagReference; 29 29 import org.orekit.files.ccsds.section.Segment; 30 30 import org.orekit.files.ccsds.utils.generation.KvnGenerator; 31 31 import org.orekit.time.AbsoluteDate; 32 + import org.orekit.time.DateComponents; 33 + import org.orekit.time.OffsetModel; 34 + import org.orekit.time.TAIUTCDatFilesLoader; 32 35 import org.orekit.time.TimeScale; 33 36 import org.orekit.time.TimeScalesFactory; 37 + import org.orekit.time.UTCTAIOffsetsLoader; 34 38 35 39 import java.io.*; 36 - import java.net.URI; 37 - import java.net.URL; 38 - import java.nio.channels.Channels; 39 - import java.nio.channels.ReadableByteChannel; 40 + import java.nio.charset.StandardCharsets; 40 41 import java.nio.file.*; 41 42 import java.util.*; 42 43 43 44 public class generate { 44 45 45 - static final String OREKIT_DATA_URL = 46 - "https://gitlab.orekit.org/orekit/orekit-data/-/archive/master/orekit-data-master.zip"; 47 - 48 - /** Bootstrap OREKIT data context. Downloads orekit-data if needed. */ 49 - static void bootstrap(Path scriptDir) throws Exception { 50 - Path dataZip = scriptDir.resolve("orekit-data-master.zip"); 51 - Path dataDir = scriptDir.resolve("orekit-data-master"); 46 + /** 47 + * Bootstrap OREKIT with minimal data (UTC-TAI leap seconds only). 48 + * We register an inline offsets loader so no external data files are 49 + * needed. 50 + */ 51 + static void bootstrap() { 52 + // UTC-TAI offsets (leap seconds through 2017-01-01) 53 + List<OffsetModel> offsets = List.of( 54 + new OffsetModel(new DateComponents(1972, 1, 1), 10), 55 + new OffsetModel(new DateComponents(1972, 7, 1), 11), 56 + new OffsetModel(new DateComponents(1973, 1, 1), 12), 57 + new OffsetModel(new DateComponents(1974, 1, 1), 13), 58 + new OffsetModel(new DateComponents(1975, 1, 1), 14), 59 + new OffsetModel(new DateComponents(1976, 1, 1), 15), 60 + new OffsetModel(new DateComponents(1977, 1, 1), 16), 61 + new OffsetModel(new DateComponents(1978, 1, 1), 17), 62 + new OffsetModel(new DateComponents(1979, 1, 1), 18), 63 + new OffsetModel(new DateComponents(1980, 1, 1), 19), 64 + new OffsetModel(new DateComponents(1981, 7, 1), 20), 65 + new OffsetModel(new DateComponents(1982, 7, 1), 21), 66 + new OffsetModel(new DateComponents(1983, 7, 1), 22), 67 + new OffsetModel(new DateComponents(1985, 7, 1), 23), 68 + new OffsetModel(new DateComponents(1988, 1, 1), 24), 69 + new OffsetModel(new DateComponents(1990, 1, 1), 25), 70 + new OffsetModel(new DateComponents(1991, 1, 1), 26), 71 + new OffsetModel(new DateComponents(1992, 7, 1), 27), 72 + new OffsetModel(new DateComponents(1993, 7, 1), 28), 73 + new OffsetModel(new DateComponents(1994, 7, 1), 29), 74 + new OffsetModel(new DateComponents(1996, 1, 1), 30), 75 + new OffsetModel(new DateComponents(1997, 7, 1), 31), 76 + new OffsetModel(new DateComponents(1999, 1, 1), 32), 77 + new OffsetModel(new DateComponents(2006, 1, 1), 33), 78 + new OffsetModel(new DateComponents(2009, 1, 1), 34), 79 + new OffsetModel(new DateComponents(2012, 7, 1), 35), 80 + new OffsetModel(new DateComponents(2015, 7, 1), 36), 81 + new OffsetModel(new DateComponents(2017, 1, 1), 37) 82 + ); 52 83 53 - if (!Files.exists(dataDir)) { 54 - if (!Files.exists(dataZip)) { 55 - System.err.println("Downloading orekit-data ..."); 56 - try (InputStream in = URI.create(OREKIT_DATA_URL).toURL().openStream()) { 57 - Files.copy(in, dataZip, StandardCopyOption.REPLACE_EXISTING); 58 - } 84 + // Register loader BEFORE any TimeScalesFactory call 85 + TimeScalesFactory.addUTCTAIOffsetsLoader(new UTCTAIOffsetsLoader() { 86 + @Override 87 + public List<OffsetModel> loadOffsets() { 88 + return offsets; 59 89 } 60 - // Unzip 61 - System.err.println("Extracting orekit-data ..."); 62 - ProcessBuilder pb = new ProcessBuilder("unzip", "-q", "-o", 63 - dataZip.toString(), "-d", scriptDir.toString()); 64 - pb.inheritIO(); 65 - int rc = pb.start().waitFor(); 66 - if (rc != 0) throw new RuntimeException("unzip failed with code " + rc); 67 - } 68 - 69 - DataProvidersManager manager = 70 - DataContext.getDefault().getDataProvidersManager(); 71 - manager.addProvider(new DirectoryCrawler(dataDir.toFile())); 90 + }); 72 91 } 73 92 74 93 // ------------------------------------------------------------------ 75 - // Scenario definitions 94 + // KVN template scenarios (CCSDS 503.0-B-2 compliant) 76 95 // ------------------------------------------------------------------ 77 96 78 - /** A test scenario: one TDM file with known measurements. */ 79 - record Scenario( 80 - String name, 81 - String originator, 82 - String participant1, 83 - String participant2, 84 - String participant3, // null if absent 85 - String mode, // null if absent 86 - String path, // null if absent 87 - String transmitBand, // null if absent 88 - String receiveBand, // null if absent 89 - String timetagRef, // null if absent 90 - Double integrationInterval, // null if absent 91 - String integrationRef, // null if absent 92 - String rangeMode, // null if absent 93 - Double rangeModulus, // null if absent 94 - String rangeUnits, // null if absent 95 - String angleType, // null if absent 96 - String dataQuality, // null if absent 97 - AbsoluteDate startTime, 98 - AbsoluteDate stopTime, 99 - Observation[] observations 100 - ) {} 97 + record Scenario(String name, String kvn, ExpectedObs[] expected) {} 101 98 102 - record Observation(ObservationType type, AbsoluteDate epoch, double value) {} 99 + record ExpectedObs(String keyword, String epoch, double value) {} 103 100 104 - static Scenario[] scenarios(TimeScale utc) { 101 + static Scenario[] scenarios() { 102 + return new Scenario[] { 105 103 106 - // --- Scenario 1: Range & Doppler (DSN-style two-way) --- 107 - AbsoluteDate t0 = new AbsoluteDate(2024, 3, 15, 10, 0, 0.0, utc); 108 - Observation[] rangeDoppler = { 109 - new Observation(ObservationType.RANGE, 110 - t0, 285432167.890), 111 - new Observation(ObservationType.RANGE, 112 - t0.shiftedBy(600.0), 285432245.123), 113 - new Observation(ObservationType.RANGE, 114 - t0.shiftedBy(1200.0), 285432322.456), 115 - new Observation(ObservationType.DOPPLER_INSTANTANEOUS, 116 - t0, -4567.123456), 117 - new Observation(ObservationType.DOPPLER_INSTANTANEOUS, 118 - t0.shiftedBy(600.0), -4567.234567), 119 - new Observation(ObservationType.DOPPLER_INSTANTANEOUS, 120 - t0.shiftedBy(1200.0), -4567.345678), 121 - }; 104 + // 1. Two-way range and Doppler (DSN-style) 105 + new Scenario("range_doppler", 106 + """ 107 + CCSDS_TDM_VERS = 1.0 108 + CREATION_DATE = 2025-01-01T00:00:00 109 + ORIGINATOR = NASA/JPL 122 110 123 - // --- Scenario 2: Angles (Az/El) --- 124 - AbsoluteDate t1 = new AbsoluteDate(2024, 6, 1, 0, 0, 0.0, utc); 125 - Observation[] angles = { 126 - new Observation(ObservationType.ANGLE_1, 127 - t1, 145.678901), 128 - new Observation(ObservationType.ANGLE_2, 129 - t1, 32.456789), 130 - new Observation(ObservationType.ANGLE_1, 131 - t1.shiftedBy(900.0), 156.789012), 132 - new Observation(ObservationType.ANGLE_2, 133 - t1.shiftedBy(900.0), 45.567890), 134 - new Observation(ObservationType.ANGLE_1, 135 - t1.shiftedBy(1800.0), 167.890123), 136 - new Observation(ObservationType.ANGLE_2, 137 - t1.shiftedBy(1800.0), 58.678901), 138 - }; 111 + META_START 112 + TIME_SYSTEM = UTC 113 + START_TIME = 2024-03-15T10:00:00 114 + STOP_TIME = 2024-03-15T10:20:00 115 + PARTICIPANT_1 = DSS-25 116 + PARTICIPANT_2 = MARS RECONNAISSANCE ORBITER 117 + MODE = SEQUENTIAL 118 + PATH = 1,2,1 119 + TRANSMIT_BAND = X 120 + RECEIVE_BAND = X 121 + TIMETAG_REF = TRANSMIT 122 + INTEGRATION_INTERVAL = 1.0 123 + INTEGRATION_REF = MIDDLE 124 + RANGE_MODE = COHERENT 125 + RANGE_UNITS = km 126 + DATA_QUALITY = RAW 127 + META_STOP 139 128 140 - // --- Scenario 3: Receive frequency --- 141 - AbsoluteDate t2 = new AbsoluteDate(2024, 9, 20, 8, 30, 0.0, utc); 142 - Observation[] freq = { 143 - new Observation(ObservationType.RECEIVE_FREQ, 144 - t2, 8420432156.789), 145 - new Observation(ObservationType.RECEIVE_FREQ, 146 - t2.shiftedBy(300.0), 8420432178.456), 147 - new Observation(ObservationType.RECEIVE_FREQ, 148 - t2.shiftedBy(600.0), 8420432200.123), 149 - }; 129 + DATA_START 130 + RANGE = 2024-03-15T10:00:00 285432167.89 131 + RANGE = 2024-03-15T10:10:00 285432245.123 132 + RANGE = 2024-03-15T10:20:00 285432322.456 133 + DOPPLER_INSTANTANEOUS = 2024-03-15T10:00:00 -4567.123456 134 + DOPPLER_INSTANTANEOUS = 2024-03-15T10:10:00 -4567.234567 135 + DOPPLER_INSTANTANEOUS = 2024-03-15T10:20:00 -4567.345678 136 + DATA_STOP 137 + """, 138 + new ExpectedObs[] { 139 + new ExpectedObs("RANGE", "2024-03-15T10:00:00", 285432167.89), 140 + new ExpectedObs("RANGE", "2024-03-15T10:10:00", 285432245.123), 141 + new ExpectedObs("RANGE", "2024-03-15T10:20:00", 285432322.456), 142 + new ExpectedObs("DOPPLER_INSTANTANEOUS", "2024-03-15T10:00:00", -4567.123456), 143 + new ExpectedObs("DOPPLER_INSTANTANEOUS", "2024-03-15T10:10:00", -4567.234567), 144 + new ExpectedObs("DOPPLER_INSTANTANEOUS", "2024-03-15T10:20:00", -4567.345678), 145 + }), 150 146 151 - // --- Scenario 4: Mixed (range + angles + Doppler) --- 152 - AbsoluteDate t3 = new AbsoluteDate(2025, 1, 1, 0, 0, 0.0, utc); 153 - Observation[] mixed = { 154 - new Observation(ObservationType.RANGE, 155 - t3, 50000.0), 156 - new Observation(ObservationType.ANGLE_1, 157 - t3, 90.0), 158 - new Observation(ObservationType.ANGLE_2, 159 - t3, 45.0), 160 - new Observation(ObservationType.DOPPLER_INSTANTANEOUS, 161 - t3, -100.5), 162 - new Observation(ObservationType.RANGE, 163 - t3.shiftedBy(60.0), 50001.234), 164 - new Observation(ObservationType.ANGLE_1, 165 - t3.shiftedBy(60.0), 90.123), 166 - new Observation(ObservationType.ANGLE_2, 167 - t3.shiftedBy(60.0), 45.456), 168 - new Observation(ObservationType.DOPPLER_INSTANTANEOUS, 169 - t3.shiftedBy(60.0), -100.678), 170 - }; 147 + // 2. Angle measurements (Az/El) 148 + new Scenario("angles_azel", 149 + """ 150 + CCSDS_TDM_VERS = 1.0 151 + CREATION_DATE = 2025-01-01T00:00:00 152 + ORIGINATOR = ESOC 171 153 172 - // --- Scenario 5: Negative and zero values --- 173 - AbsoluteDate t4 = new AbsoluteDate(2024, 12, 31, 23, 59, 0.0, utc); 174 - Observation[] edgeValues = { 175 - new Observation(ObservationType.RANGE, 176 - t4, 0.0), 177 - new Observation(ObservationType.DOPPLER_INSTANTANEOUS, 178 - t4, -99999.999999), 179 - new Observation(ObservationType.DOPPLER_INSTANTANEOUS, 180 - t4.shiftedBy(1.0), 99999.999999), 181 - new Observation(ObservationType.RANGE, 182 - t4.shiftedBy(1.0), 1e12), 183 - }; 154 + META_START 155 + TIME_SYSTEM = UTC 156 + START_TIME = 2024-06-01T00:00:00 157 + STOP_TIME = 2024-06-01T00:30:00 158 + PARTICIPANT_1 = MALINDI 159 + PARTICIPANT_2 = SENTINEL-1A 160 + MODE = SEQUENTIAL 161 + PATH = 1,2,1 162 + ANGLE_TYPE = AZEL 163 + DATA_QUALITY = VALIDATED 164 + META_STOP 184 165 185 - // --- Scenario 6: Three participants (relay) --- 186 - AbsoluteDate t5 = new AbsoluteDate(2024, 7, 4, 12, 0, 0.0, utc); 187 - Observation[] relay = { 188 - new Observation(ObservationType.RANGE, 189 - t5, 384400.0), 190 - new Observation(ObservationType.RANGE, 191 - t5.shiftedBy(120.0), 384401.567), 192 - }; 166 + DATA_START 167 + ANGLE_1 = 2024-06-01T00:00:00 145.678901 168 + ANGLE_2 = 2024-06-01T00:00:00 32.456789 169 + ANGLE_1 = 2024-06-01T00:15:00 156.789012 170 + ANGLE_2 = 2024-06-01T00:15:00 45.56789 171 + ANGLE_1 = 2024-06-01T00:30:00 167.890123 172 + ANGLE_2 = 2024-06-01T00:30:00 58.678901 173 + DATA_STOP 174 + """, 175 + new ExpectedObs[] { 176 + new ExpectedObs("ANGLE_1", "2024-06-01T00:00:00", 145.678901), 177 + new ExpectedObs("ANGLE_2", "2024-06-01T00:00:00", 32.456789), 178 + new ExpectedObs("ANGLE_1", "2024-06-01T00:15:00", 156.789012), 179 + new ExpectedObs("ANGLE_2", "2024-06-01T00:15:00", 45.56789), 180 + new ExpectedObs("ANGLE_1", "2024-06-01T00:30:00", 167.890123), 181 + new ExpectedObs("ANGLE_2", "2024-06-01T00:30:00", 58.678901), 182 + }), 193 183 194 - // --- Scenario 7: Small fractional seconds --- 195 - AbsoluteDate t6 = new AbsoluteDate(2024, 2, 29, 12, 0, 0.123, utc); 196 - Observation[] fractional = { 197 - new Observation(ObservationType.RANGE, 198 - t6, 12345.6789), 199 - new Observation(ObservationType.RANGE, 200 - t6.shiftedBy(0.5), 12345.789), 201 - new Observation(ObservationType.RANGE, 202 - t6.shiftedBy(1.0), 12345.8901), 203 - }; 184 + // 3. Receive frequency 185 + new Scenario("receive_freq", 186 + """ 187 + CCSDS_TDM_VERS = 1.0 188 + CREATION_DATE = 2025-01-01T00:00:00 189 + ORIGINATOR = CNES 204 190 205 - return new Scenario[] { 206 - new Scenario("range_doppler", "NASA/JPL", 207 - "DSS-25", "MARS RECONNAISSANCE ORBITER", 208 - null, "SEQUENTIAL", "1,2,1", 209 - "X", "X", "TRANSMIT", 210 - 1.0, "MIDDLE", 211 - "COHERENT", null, "km", 212 - null, "RAW", 213 - t0, t0.shiftedBy(1200.0), rangeDoppler), 191 + META_START 192 + TIME_SYSTEM = UTC 193 + START_TIME = 2024-09-20T08:30:00 194 + STOP_TIME = 2024-09-20T08:40:00 195 + PARTICIPANT_1 = TOULOUSE 196 + PARTICIPANT_2 = JASON-3 197 + MODE = SEQUENTIAL 198 + PATH = 1,2,1 199 + TRANSMIT_BAND = X 200 + RECEIVE_BAND = X 201 + TIMETAG_REF = RECEIVE 202 + META_STOP 214 203 215 - new Scenario("angles_azel", "ESOC", 216 - "MALINDI", "SENTINEL-1A", 217 - null, null, null, 218 - null, null, null, 219 - null, null, 220 - null, null, null, 221 - "AZEL", "VALIDATED", 222 - t1, t1.shiftedBy(1800.0), angles), 204 + DATA_START 205 + RECEIVE_FREQ = 2024-09-20T08:30:00 8420432156.789 206 + RECEIVE_FREQ = 2024-09-20T08:35:00 8420432178.456 207 + RECEIVE_FREQ = 2024-09-20T08:40:00 8420432200.123 208 + DATA_STOP 209 + """, 210 + new ExpectedObs[] { 211 + new ExpectedObs("RECEIVE_FREQ", "2024-09-20T08:30:00", 8420432156.789), 212 + new ExpectedObs("RECEIVE_FREQ", "2024-09-20T08:35:00", 8420432178.456), 213 + new ExpectedObs("RECEIVE_FREQ", "2024-09-20T08:40:00", 8420432200.123), 214 + }), 223 215 224 - new Scenario("receive_freq", "CNES", 225 - "TOULOUSE", "JASON-3", 226 - null, "SEQUENTIAL", "1,2,1", 227 - "X", "X", "RECEIVE", 228 - null, null, 229 - null, null, null, 230 - null, null, 231 - t2, t2.shiftedBy(600.0), freq), 216 + // 4. Mixed observations (range + angles + Doppler) 217 + new Scenario("mixed", 218 + """ 219 + CCSDS_TDM_VERS = 1.0 220 + CREATION_DATE = 2025-01-01T00:00:00 221 + ORIGINATOR = DLR 232 222 233 - new Scenario("mixed", "DLR", 234 - "WEILHEIM", "TERRASAR-X", 235 - null, "SEQUENTIAL", "1,2,1", 236 - "X", "X", "TRANSMIT", 237 - 1.0, "START", 238 - "COHERENT", null, "km", 239 - "AZEL", "RAW", 240 - t3, t3.shiftedBy(60.0), mixed), 223 + META_START 224 + TIME_SYSTEM = UTC 225 + START_TIME = 2025-01-01T00:00:00 226 + STOP_TIME = 2025-01-01T00:01:00 227 + PARTICIPANT_1 = WEILHEIM 228 + PARTICIPANT_2 = TERRASAR-X 229 + MODE = SEQUENTIAL 230 + PATH = 1,2,1 231 + TRANSMIT_BAND = X 232 + RECEIVE_BAND = X 233 + TIMETAG_REF = TRANSMIT 234 + INTEGRATION_INTERVAL = 1.0 235 + INTEGRATION_REF = START 236 + RANGE_MODE = COHERENT 237 + RANGE_UNITS = km 238 + ANGLE_TYPE = AZEL 239 + DATA_QUALITY = RAW 240 + META_STOP 241 241 242 - new Scenario("edge_values", "NASA/GSFC", 243 - "WHITESANDS", "TDRS-M", 244 - null, "SEQUENTIAL", "1,2,1", 245 - "S", "S", "TRANSMIT", 246 - null, null, 247 - null, null, "km", 248 - null, null, 249 - t4, t4.shiftedBy(1.0), edgeValues), 242 + DATA_START 243 + RANGE = 2025-01-01T00:00:00 50000.0 244 + ANGLE_1 = 2025-01-01T00:00:00 90.0 245 + ANGLE_2 = 2025-01-01T00:00:00 45.0 246 + DOPPLER_INSTANTANEOUS = 2025-01-01T00:00:00 -100.5 247 + RANGE = 2025-01-01T00:01:00 50001.234 248 + ANGLE_1 = 2025-01-01T00:01:00 90.123 249 + ANGLE_2 = 2025-01-01T00:01:00 45.456 250 + DOPPLER_INSTANTANEOUS = 2025-01-01T00:01:00 -100.678 251 + DATA_STOP 252 + """, 253 + new ExpectedObs[] { 254 + new ExpectedObs("RANGE", "2025-01-01T00:00:00", 50000.0), 255 + new ExpectedObs("ANGLE_1", "2025-01-01T00:00:00", 90.0), 256 + new ExpectedObs("ANGLE_2", "2025-01-01T00:00:00", 45.0), 257 + new ExpectedObs("DOPPLER_INSTANTANEOUS", "2025-01-01T00:00:00", -100.5), 258 + new ExpectedObs("RANGE", "2025-01-01T00:01:00", 50001.234), 259 + new ExpectedObs("ANGLE_1", "2025-01-01T00:01:00", 90.123), 260 + new ExpectedObs("ANGLE_2", "2025-01-01T00:01:00", 45.456), 261 + new ExpectedObs("DOPPLER_INSTANTANEOUS", "2025-01-01T00:01:00", -100.678), 262 + }), 250 263 251 - new Scenario("three_participants", "NASA/JPL", 252 - "DSS-14", "LUNAR-GATEWAY", "ORION", 253 - "SEQUENTIAL", "1,2,3", 254 - "Ka", "Ka", "TRANSMIT", 255 - null, null, 256 - "COHERENT", null, "km", 257 - null, null, 258 - t5, t5.shiftedBy(120.0), relay), 264 + // 5. Edge values (zero, negative, large) 265 + new Scenario("edge_values", 266 + """ 267 + CCSDS_TDM_VERS = 1.0 268 + CREATION_DATE = 2025-01-01T00:00:00 269 + ORIGINATOR = NASA/GSFC 259 270 260 - new Scenario("fractional_seconds", "JAXA", 261 - "USUDA", "HAYABUSA2", 262 - null, null, "1,2,1", 263 - "X", "X", null, 264 - null, null, 265 - null, null, "km", 266 - null, null, 267 - t6, t6.shiftedBy(1.0), fractional), 268 - }; 269 - } 271 + META_START 272 + TIME_SYSTEM = UTC 273 + START_TIME = 2024-12-31T23:59:00 274 + STOP_TIME = 2024-12-31T23:59:01 275 + PARTICIPANT_1 = WHITESANDS 276 + PARTICIPANT_2 = TDRS-M 277 + MODE = SEQUENTIAL 278 + PATH = 1,2,1 279 + TRANSMIT_BAND = S 280 + RECEIVE_BAND = S 281 + TIMETAG_REF = TRANSMIT 282 + RANGE_UNITS = km 283 + META_STOP 270 284 271 - // ------------------------------------------------------------------ 272 - // TDM file building via OREKIT public API 273 - // ------------------------------------------------------------------ 285 + DATA_START 286 + RANGE = 2024-12-31T23:59:00 0.0 287 + DOPPLER_INSTANTANEOUS = 2024-12-31T23:59:00 -99999.999999 288 + DOPPLER_INSTANTANEOUS = 2024-12-31T23:59:01 99999.999999 289 + RANGE = 2024-12-31T23:59:01 1000000000000.0 290 + DATA_STOP 291 + """, 292 + new ExpectedObs[] { 293 + new ExpectedObs("RANGE", "2024-12-31T23:59:00", 0.0), 294 + new ExpectedObs("DOPPLER_INSTANTANEOUS", "2024-12-31T23:59:00", -99999.999999), 295 + new ExpectedObs("DOPPLER_INSTANTANEOUS", "2024-12-31T23:59:01", 99999.999999), 296 + new ExpectedObs("RANGE", "2024-12-31T23:59:01", 1000000000000.0), 297 + }), 274 298 275 - static TdmFile buildTdm(Scenario sc, TimeScale utc) { 276 - TdmHeader header = new TdmHeader(); 277 - header.setFormatVersion(1.0); 278 - header.setCreationDate(new AbsoluteDate(2025, 1, 1, 0, 0, 0.0, utc)); 279 - header.setOriginator(sc.originator()); 299 + // 6. Three participants (relay scenario) 300 + new Scenario("three_participants", 301 + """ 302 + CCSDS_TDM_VERS = 1.0 303 + CREATION_DATE = 2025-01-01T00:00:00 304 + ORIGINATOR = NASA/JPL 280 305 281 - TdmMetadata meta = new TdmMetadata(); 282 - meta.setTimeSystem(TimeSystem.UTC); 283 - meta.setStartTime(sc.startTime()); 284 - meta.setStopTime(sc.stopTime()); 285 - meta.setParticipants(buildParticipants(sc)); 306 + META_START 307 + TIME_SYSTEM = UTC 308 + START_TIME = 2024-07-04T12:00:00 309 + STOP_TIME = 2024-07-04T12:02:00 310 + PARTICIPANT_1 = DSS-14 311 + PARTICIPANT_2 = LUNAR-GATEWAY 312 + PARTICIPANT_3 = ORION 313 + MODE = SEQUENTIAL 314 + PATH = 1,2,3 315 + TRANSMIT_BAND = Ka 316 + RECEIVE_BAND = Ka 317 + TIMETAG_REF = TRANSMIT 318 + RANGE_MODE = COHERENT 319 + RANGE_UNITS = km 320 + META_STOP 286 321 287 - if (sc.mode() != null) { 288 - meta.setMode(sc.mode()); 289 - } 290 - if (sc.path() != null) { 291 - // Parse "1,2,1" into int array 292 - String[] parts = sc.path().split(","); 293 - int[] pathArr = new int[parts.length]; 294 - for (int i = 0; i < parts.length; i++) { 295 - pathArr[i] = Integer.parseInt(parts[i].trim()); 296 - } 297 - meta.setPath(pathArr); 298 - } 299 - if (sc.transmitBand() != null) meta.setTransmitBand(sc.transmitBand()); 300 - if (sc.receiveBand() != null) meta.setReceiveBand(sc.receiveBand()); 301 - if (sc.timetagRef() != null) { 302 - meta.setTimetagRef(TimetagReference.valueOf(sc.timetagRef())); 303 - } 304 - if (sc.integrationInterval() != null) { 305 - meta.setIntegrationInterval(sc.integrationInterval()); 306 - } 307 - if (sc.integrationRef() != null) { 308 - meta.setIntegrationRef(IntegrationReference.valueOf(sc.integrationRef())); 309 - } 310 - if (sc.rangeMode() != null) { 311 - meta.setRangeMode(RangeMode.valueOf(sc.rangeMode())); 312 - } 313 - if (sc.rangeModulus() != null) { 314 - meta.setRangeModulus(sc.rangeModulus()); 315 - } 316 - if (sc.rangeUnits() != null) { 317 - meta.setRangeUnits(RangeUnits.valueOf(sc.rangeUnits().toLowerCase())); 318 - } 319 - if (sc.angleType() != null) { 320 - meta.setAngleType(AngleType.valueOf(sc.angleType())); 321 - } 322 - if (sc.dataQuality() != null) { 323 - meta.setDataQuality(sc.dataQuality()); 324 - } 322 + DATA_START 323 + RANGE = 2024-07-04T12:00:00 384400.0 324 + RANGE = 2024-07-04T12:02:00 384401.567 325 + DATA_STOP 326 + """, 327 + new ExpectedObs[] { 328 + new ExpectedObs("RANGE", "2024-07-04T12:00:00", 384400.0), 329 + new ExpectedObs("RANGE", "2024-07-04T12:02:00", 384401.567), 330 + }), 325 331 326 - ObservationsBlock block = new ObservationsBlock(); 327 - block.setMetadata(meta); 328 - for (Observation obs : sc.observations()) { 329 - block.addObservation(obs.type(), obs.epoch(), obs.value()); 330 - } 332 + // 7. Fractional seconds in epochs 333 + new Scenario("fractional_seconds", 334 + """ 335 + CCSDS_TDM_VERS = 1.0 336 + CREATION_DATE = 2025-01-01T00:00:00 337 + ORIGINATOR = JAXA 331 338 332 - List<Segment<TdmMetadata, ObservationsBlock>> segments = new ArrayList<>(); 333 - segments.add(new Segment<>(meta, block)); 339 + META_START 340 + TIME_SYSTEM = UTC 341 + START_TIME = 2024-02-29T12:00:00.123 342 + STOP_TIME = 2024-02-29T12:00:01.123 343 + PARTICIPANT_1 = USUDA 344 + PARTICIPANT_2 = HAYABUSA2 345 + MODE = SEQUENTIAL 346 + PATH = 1,2,1 347 + TRANSMIT_BAND = X 348 + RECEIVE_BAND = X 349 + RANGE_UNITS = km 350 + META_STOP 334 351 335 - return new TdmFile(header, segments, null, null); 352 + DATA_START 353 + RANGE = 2024-02-29T12:00:00.123 12345.6789 354 + RANGE = 2024-02-29T12:00:00.623 12345.789 355 + RANGE = 2024-02-29T12:00:01.123 12345.8901 356 + DATA_STOP 357 + """, 358 + new ExpectedObs[] { 359 + new ExpectedObs("RANGE", "2024-02-29T12:00:00.123", 12345.6789), 360 + new ExpectedObs("RANGE", "2024-02-29T12:00:00.623", 12345.789), 361 + new ExpectedObs("RANGE", "2024-02-29T12:00:01.123", 12345.8901), 362 + }), 363 + }; 336 364 } 337 365 338 - static Map<Integer, String> buildParticipants(Scenario sc) { 339 - Map<Integer, String> p = new TreeMap<>(); 340 - p.put(1, sc.participant1()); 341 - p.put(2, sc.participant2()); 342 - if (sc.participant3() != null) { 343 - p.put(3, sc.participant3()); 344 - } 345 - return p; 366 + // ------------------------------------------------------------------ 367 + // Parse with OREKIT, re-serialize, and write traces 368 + // ------------------------------------------------------------------ 369 + 370 + static Tdm parseKvn(String kvn) { 371 + TdmParser parser = new ParserBuilder().buildTdmParser(); 372 + DataSource source = new DataSource("<inline>", 373 + () -> new ByteArrayInputStream(kvn.getBytes(StandardCharsets.UTF_8))); 374 + return parser.parseMessage(source); 346 375 } 347 376 348 - /** Write a TdmFile to KVN format using TdmWriter + KvnGenerator. */ 349 - static String writeKvn(TdmFile tdm) throws Exception { 377 + static String reserialize(Tdm tdm) throws Exception { 350 378 StringWriter sw = new StringWriter(); 351 - // TdmWriter(writerId, conventions, dataContext) 352 - TdmWriter writer = new TdmWriter( 353 - null, null, DataContext.getDefault()); 354 - try (KvnGenerator gen = new KvnGenerator(sw, 0, "generate.java", 355 - Constants.MISSING, false)) { 379 + TdmWriter writer = new WriterBuilder().buildTdmWriter(); 380 + // KvnGenerator(Appendable, int indentation, String outputName, 381 + // double maxRelativeOffset, int unitsColumn) 382 + try (KvnGenerator gen = new KvnGenerator(sw, 0, "", Double.NaN, 0)) { 356 383 writer.writeMessage(gen, tdm); 357 384 } 358 385 return sw.toString(); 359 386 } 360 387 361 - // OREKIT constants placeholder 362 - static final class Constants { 363 - static final double MISSING = Double.NaN; 388 + /** Extract observation values from a parsed TDM for the CSV index. */ 389 + static List<ExpectedObs> extractObs(Tdm tdm, TimeScale utc) { 390 + List<ExpectedObs> result = new ArrayList<>(); 391 + for (var segment : tdm.getSegments()) { 392 + ObservationsBlock block = segment.getData(); 393 + for (Observation obs : block.getObservations()) { 394 + String keyword = obs.getType().name(); 395 + AbsoluteDate epoch = obs.getEpoch(); 396 + double value = obs.getMeasurement(); 397 + String epochStr = epoch.toString(utc); 398 + result.add(new ExpectedObs(keyword, epochStr, value)); 399 + } 400 + } 401 + return result; 364 402 } 365 403 366 404 // ------------------------------------------------------------------ 367 - // CSV index generation 405 + // CSV index 368 406 // ------------------------------------------------------------------ 369 407 370 - static void writeCsvIndex(Scenario[] scenarios, Path traceDir) throws Exception { 408 + static void writeCsvIndex(List<IndexRow> rows, Path traceDir) 409 + throws Exception { 371 410 try (PrintWriter pw = new PrintWriter(new FileWriter( 372 411 traceDir.resolve("index.csv").toFile()))) { 373 412 pw.println("name,file,participant_1,participant_2,participant_3," 374 - + "mode,path,transmit_band,receive_band," 375 - + "range_mode,range_units,angle_type,data_quality," 376 - + "num_observations,obs_keywords,obs_values"); 377 - 378 - for (Scenario sc : scenarios) { 379 - StringBuilder keywords = new StringBuilder(); 380 - StringBuilder values = new StringBuilder(); 381 - for (int i = 0; i < sc.observations().length; i++) { 382 - if (i > 0) { keywords.append(";"); values.append(";"); } 383 - keywords.append(sc.observations()[i].type().name()); 384 - values.append(String.format("%.14g", sc.observations()[i].value())); 385 - } 413 + + "num_observations,obs_keywords,obs_epochs,obs_values"); 386 414 387 - pw.printf("%s,%s.kvn,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%d,\"%s\",\"%s\"%n", 388 - sc.name(), 389 - sc.name(), 390 - sc.participant1(), 391 - sc.participant2(), 392 - sc.participant3() != null ? sc.participant3() : "", 393 - sc.mode() != null ? sc.mode() : "", 394 - sc.path() != null ? sc.path() : "", 395 - sc.transmitBand() != null ? sc.transmitBand() : "", 396 - sc.receiveBand() != null ? sc.receiveBand() : "", 397 - sc.rangeMode() != null ? sc.rangeMode() : "", 398 - sc.rangeUnits() != null ? sc.rangeUnits() : "", 399 - sc.angleType() != null ? sc.angleType() : "", 400 - sc.dataQuality() != null ? sc.dataQuality() : "", 401 - sc.observations().length, 402 - keywords.toString(), 403 - values.toString()); 415 + for (IndexRow r : rows) { 416 + pw.printf("%s,%s,%s,%s,%s,%d,\"%s\",\"%s\",\"%s\"%n", 417 + r.name, r.file, 418 + r.participant1, r.participant2, r.participant3, 419 + r.numObs, 420 + r.obsKeywords, r.obsEpochs, r.obsValues); 404 421 } 405 422 } 406 423 } 407 424 425 + record IndexRow( 426 + String name, String file, 427 + String participant1, String participant2, String participant3, 428 + int numObs, 429 + String obsKeywords, String obsEpochs, String obsValues 430 + ) {} 431 + 432 + static IndexRow buildIndexRow(String name, Tdm tdm, TimeScale utc) { 433 + var seg = tdm.getSegments().get(0); 434 + TdmMetadata meta = seg.getMetadata(); 435 + Map<Integer, String> participants = meta.getParticipants(); 436 + String p1 = participants.getOrDefault(1, ""); 437 + String p2 = participants.getOrDefault(2, ""); 438 + String p3 = participants.getOrDefault(3, ""); 439 + 440 + List<ExpectedObs> obs = extractObs(tdm, utc); 441 + StringBuilder keywords = new StringBuilder(); 442 + StringBuilder epochs = new StringBuilder(); 443 + StringBuilder values = new StringBuilder(); 444 + for (int i = 0; i < obs.size(); i++) { 445 + if (i > 0) { keywords.append(";"); epochs.append(";"); values.append(";"); } 446 + keywords.append(obs.get(i).keyword()); 447 + epochs.append(obs.get(i).epoch()); 448 + values.append(String.format("%.14g", obs.get(i).value())); 449 + } 450 + 451 + return new IndexRow(name, name + ".kvn", 452 + p1, p2, p3, 453 + obs.size(), 454 + keywords.toString(), epochs.toString(), values.toString()); 455 + } 456 + 408 457 // ------------------------------------------------------------------ 409 458 // Main 410 459 // ------------------------------------------------------------------ ··· 417 466 Path traceDir = Paths.get(args[0]); 418 467 Files.createDirectories(traceDir); 419 468 420 - // Bootstrap OREKIT data 421 - Path scriptDir = Paths.get(System.getProperty("user.dir")); 422 - bootstrap(scriptDir); 469 + // Bootstrap OREKIT with minimal data 470 + bootstrap(); 423 471 424 472 TimeScale utc = TimeScalesFactory.getUTC(); 425 - Scenario[] allScenarios = scenarios(utc); 473 + Scenario[] allScenarios = scenarios(); 474 + List<IndexRow> indexRows = new ArrayList<>(); 426 475 427 - // Generate KVN files via OREKIT 428 476 for (Scenario sc : allScenarios) { 429 - TdmFile tdm = buildTdm(sc, utc); 430 - String kvn = writeKvn(tdm); 477 + // Parse with OREKIT (validates CCSDS compliance) 478 + Tdm tdm = parseKvn(sc.kvn()); 479 + 480 + // Sanity-check: OREKIT extracted the expected observation count 481 + int parsedCount = 0; 482 + for (var seg : tdm.getSegments()) { 483 + parsedCount += seg.getData().getObservations().size(); 484 + } 485 + if (parsedCount != sc.expected().length) { 486 + throw new RuntimeException(sc.name() + ": expected " 487 + + sc.expected().length + " observations, OREKIT parsed " 488 + + parsedCount); 489 + } 490 + 491 + // Re-serialize through OREKIT's KVN writer 492 + String kvnOut = reserialize(tdm); 431 493 Path outPath = traceDir.resolve(sc.name() + ".kvn"); 432 - Files.writeString(outPath, kvn); 494 + Files.writeString(outPath, kvnOut); 433 495 System.out.println("Wrote " + outPath); 496 + 497 + // Build index row from OREKIT-parsed data 498 + indexRows.add(buildIndexRow(sc.name(), tdm, utc)); 434 499 } 435 500 436 - // Generate CSV index 437 - writeCsvIndex(allScenarios, traceDir); 501 + // Write CSV index 502 + writeCsvIndex(indexRows, traceDir); 438 503 System.out.println("Wrote " + traceDir.resolve("index.csv")); 439 504 } 440 505 }
+3 -1
test/interop/orekit/scripts/generate.sh
··· 2 2 set -euo pipefail 3 3 command -v jbang >/dev/null || { echo "jbang not on PATH" >&2; exit 1; } 4 4 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 5 - TRACE_DIR="$(cd "$SCRIPT_DIR/../traces" && pwd)" 5 + TRACE_DIR="$SCRIPT_DIR/../traces" 6 + mkdir -p "$TRACE_DIR" 7 + TRACE_DIR="$(cd "$TRACE_DIR" && pwd)" 6 8 7 9 cd "$SCRIPT_DIR" 8 10 jbang generate.java "$TRACE_DIR"
+161
test/interop/orekit/test.ml
··· 1 + (** OREKIT interop tests for ocaml-tdm. 2 + 3 + Tests TDM KVN parsing against OREKIT 12.2 (Java), ESA's open-source 4 + flight dynamics library. 5 + 6 + Traces generated by: OREKIT 12.2 via jbang 7 + Regenerate: dune build @regen-traces *) 8 + 9 + let trace path = Filename.concat "traces" path 10 + 11 + (* -- CSV index codec ------------------------------------------------- *) 12 + 13 + type index_row = { 14 + name : string; 15 + file : string; 16 + participant_1 : string; 17 + participant_2 : string; 18 + participant_3 : string; 19 + num_observations : int; 20 + obs_keywords : string; 21 + obs_epochs : string; 22 + obs_values : string; 23 + } 24 + 25 + let index_codec = 26 + Csvt.( 27 + Row.( 28 + obj 29 + (fun 30 + name 31 + file 32 + participant_1 33 + participant_2 34 + participant_3 35 + num_observations 36 + obs_keywords 37 + obs_epochs 38 + obs_values 39 + -> 40 + { 41 + name; 42 + file; 43 + participant_1; 44 + participant_2; 45 + participant_3; 46 + num_observations; 47 + obs_keywords; 48 + obs_epochs; 49 + obs_values; 50 + }) 51 + |> col "name" string ~enc:(fun r -> r.name) 52 + |> col "file" string ~enc:(fun r -> r.file) 53 + |> col "participant_1" string ~enc:(fun r -> r.participant_1) 54 + |> col "participant_2" string ~enc:(fun r -> r.participant_2) 55 + |> col "participant_3" string ~enc:(fun r -> r.participant_3) 56 + |> col "num_observations" int ~enc:(fun r -> r.num_observations) 57 + |> col "obs_keywords" string ~enc:(fun r -> r.obs_keywords) 58 + |> col "obs_epochs" string ~enc:(fun r -> r.obs_epochs) 59 + |> col "obs_values" string ~enc:(fun r -> r.obs_values) 60 + |> finish)) 61 + 62 + let parse_index () = 63 + match Csvt.decode_file index_codec (trace "index.csv") with 64 + | Ok rows -> rows 65 + | Error e -> Alcotest.failf "CSV index: %a" Csvt.pp_error e 66 + 67 + (* -- Helpers --------------------------------------------------------- *) 68 + 69 + let eps = 1e-6 70 + let split_semi s = if s = "" then [] else String.split_on_char ';' s 71 + 72 + let check_float msg expected actual = 73 + if Float.is_nan expected && Float.is_nan actual then () 74 + else 75 + let diff = Float.abs (expected -. actual) in 76 + let scale = Float.max 1.0 (Float.abs expected) in 77 + if diff /. scale > eps then 78 + Alcotest.failf "%s: expected %.14g, got %.14g (diff %.14g)" msg expected 79 + actual diff 80 + 81 + (* -- Parse and verify ------------------------------------------------ *) 82 + 83 + let test_parse (row : index_row) () = 84 + let path = trace row.file in 85 + let tdm = 86 + match Tdm.of_file path with 87 + | Ok t -> t 88 + | Error e -> Alcotest.failf "%s: parse failed: %a" row.name Tdm.pp_error e 89 + in 90 + 91 + (* Check participants *) 92 + Alcotest.(check string) 93 + (row.name ^ ": participant_1") 94 + row.participant_1 tdm.metadata.participant_1; 95 + Alcotest.(check string) 96 + (row.name ^ ": participant_2") 97 + row.participant_2 tdm.metadata.participant_2; 98 + if row.participant_3 <> "" then 99 + Alcotest.(check (option string)) 100 + (row.name ^ ": participant_3") 101 + (Some row.participant_3) tdm.metadata.participant_3; 102 + 103 + (* Check observation count *) 104 + Alcotest.(check int) 105 + (row.name ^ ": num_observations") 106 + row.num_observations 107 + (List.length tdm.observations); 108 + 109 + (* Check each observation *) 110 + let expected_keywords = split_semi row.obs_keywords in 111 + let expected_values = split_semi row.obs_values in 112 + List.iteri 113 + (fun i obs -> 114 + let exp_kw = List.nth expected_keywords i in 115 + let exp_val = float_of_string (List.nth expected_values i) in 116 + let tag = Printf.sprintf "%s[%d]" row.name i in 117 + Alcotest.(check string) (tag ^ ": keyword") exp_kw obs.Tdm.keyword; 118 + check_float (tag ^ ": value") exp_val obs.Tdm.value) 119 + tdm.observations 120 + 121 + (* -- Roundtrip: parse OREKIT KVN, serialize, re-parse ---------------- *) 122 + 123 + let test_roundtrip (row : index_row) () = 124 + let path = trace row.file in 125 + let tdm1 = 126 + match Tdm.of_file path with 127 + | Ok t -> t 128 + | Error e -> 129 + Alcotest.failf "%s: roundtrip parse1 failed: %a" row.name Tdm.pp_error e 130 + in 131 + let kvn = Tdm.to_string tdm1 in 132 + let tdm2 = 133 + match Tdm.of_string kvn with 134 + | Ok t -> t 135 + | Error e -> 136 + Alcotest.failf "%s: roundtrip parse2 failed: %a" row.name Tdm.pp_error e 137 + in 138 + Alcotest.(check int) 139 + (row.name ^ ": roundtrip obs count") 140 + (List.length tdm1.observations) 141 + (List.length tdm2.observations); 142 + List.iter2 143 + (fun o1 o2 -> 144 + Alcotest.(check string) "keyword" o1.Tdm.keyword o2.Tdm.keyword; 145 + check_float "value" o1.value o2.value) 146 + tdm1.observations tdm2.observations 147 + 148 + (* -- Main ------------------------------------------------------------ *) 149 + 150 + let () = 151 + let rows = parse_index () in 152 + Alcotest.run "tdm-interop" 153 + [ 154 + ( "parse", 155 + List.map (fun r -> Alcotest.test_case r.name `Quick (test_parse r)) rows 156 + ); 157 + ( "roundtrip", 158 + List.map 159 + (fun r -> Alcotest.test_case r.name `Quick (test_roundtrip r)) 160 + rows ); 161 + ]