1   /* Copyright 2002-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.estimation.measurements.gnss;
18  
19  import java.io.IOException;
20  import java.security.NoSuchAlgorithmException;
21  import java.util.ArrayList;
22  import java.util.List;
23  
24  import org.junit.jupiter.api.Assertions;
25  import org.junit.jupiter.api.BeforeEach;
26  import org.junit.jupiter.api.Test;
27  import org.orekit.Utils;
28  import org.orekit.data.DataSource;
29  import org.orekit.errors.OrekitException;
30  import org.orekit.errors.OrekitMessages;
31  import org.orekit.files.rinex.observation.ObservationData;
32  import org.orekit.files.rinex.observation.ObservationDataSet;
33  import org.orekit.files.rinex.observation.RinexObservationParser;
34  import org.orekit.gnss.GnssSignal;
35  import org.orekit.gnss.MeasurementType;
36  import org.orekit.gnss.PredefinedObservationType;
37  import org.orekit.gnss.SatInSystem;
38  import org.orekit.gnss.SatelliteSystem;
39  import org.orekit.utils.Constants;
40  
41  public class MeasurementCombinationFactoryTest {
42  
43      /** Threshold for test acceptance. */
44      private static final double eps = 1.0e-4;
45  
46      /** First observation data. */
47      private ObservationData obs1;
48  
49      /** Satellite system used for the tests. */
50      private SatelliteSystem system;
51  
52      /** RINEX 2 Observation data set. */
53      private ObservationDataSet dataSetRinex2;
54  
55      /** RINEX 3 Observation data set. */
56      private ObservationDataSet dataSetRinex3;
57  
58      @BeforeEach
59      public void setUp() throws NoSuchAlgorithmException, IOException {
60          Utils.setDataRoot("gnss");
61          RinexObservationParser parser = new RinexObservationParser();
62  
63          // Observation data
64          obs1 = new ObservationData(PredefinedObservationType.L1, 2.25E7, 0, 0);
65  
66          // RINEX 2 Observation data set
67          final String name2 = "rinex/truncate-sbch0440.16o";
68          List<ObservationDataSet> parsed2 = parser.parse(new DataSource(name2,
69                                                                         () -> Utils.class.getClassLoader().getResourceAsStream(name2))).
70                                             getObservationDataSets();
71          dataSetRinex2 = parsed2.get(0);
72  
73          // RINEX 3 Observation data set
74          final String name3 = "rinex/aaaa0000.00o";
75          List<ObservationDataSet> parsed3 = parser.parse(new DataSource(name3,
76                                                                         () -> Utils.class.getClassLoader().getResourceAsStream(name3))).
77                                             getObservationDataSets();
78          dataSetRinex3 = parsed3.get(1);
79  
80          // Satellite system
81          system = dataSetRinex2.getSatellite().getSystem();
82      }
83  
84      @Test
85      public void testEmptyDataSetGeometryFree() {
86          doTestEmptyDataSet(MeasurementCombinationFactory.getGeometryFreeCombination(system));
87      }
88  
89      @Test
90      public void testEmptyDataSetIonoFree() {
91          doTestEmptyDataSet(MeasurementCombinationFactory.getIonosphereFreeCombination(system));
92      }
93  
94      @Test
95      public void testEmptyDataSetWideLane() {
96          doTestEmptyDataSet(MeasurementCombinationFactory.getWideLaneCombination(system));
97      }
98  
99      @Test
100     public void testEmptyDataSetNarrowLane() {
101         doTestEmptyDataSet(MeasurementCombinationFactory.getNarrowLaneCombination(system));
102     }
103 
104     @Test
105     public void testEmptyDataSetMelbourneWubbena() {
106         doTestEmptyDataSet(MeasurementCombinationFactory.getMelbourneWubbenaCombination(system));
107     }
108 
109     @Test
110     public void testEmptyDataSetPhaseMinusCode() {
111         doTestEmptyDataSet(MeasurementCombinationFactory.getPhaseMinusCodeCombination(system));
112     }
113 
114     @Test
115     public void testEmptyDataSetGRAPHIC() {
116         doTestEmptyDataSet(MeasurementCombinationFactory.getGRAPHICCombination(system));
117     }
118 
119     /**
120      * Test code stability if an empty observation data set is used.
121      */
122     private void doTestEmptyDataSet(final MeasurementCombination combination) {
123         // Build empty observation data set
124         final ObservationDataSet emptyDataSet = new ObservationDataSet(new SatInSystem(dataSetRinex2.getSatellite().getSystem(),
125                                                                                        dataSetRinex2.getSatellite().getPRN()),
126                                                                        dataSetRinex2.getDate(), 0,
127                                                                        dataSetRinex2.getRcvrClkOffset(),
128                                                                        new ArrayList<>());
129         // Test first method signature
130         final CombinedObservationDataSet combinedData = combination.combine(emptyDataSet);
131         Assertions.assertEquals(0, combinedData.getObservationData().size());
132     }
133 
134     @Test
135     public void testExceptionsGeometryFree() {
136         doTestExceptionsDualFrequency(MeasurementCombinationFactory.getGeometryFreeCombination(system));
137     }
138 
139     @Test
140     public void testExceptionsIonoFree() {
141         doTestExceptionsDualFrequency(MeasurementCombinationFactory.getIonosphereFreeCombination(system));
142     }
143 
144     @Test
145     public void testExceptionsWideLane() {
146         doTestExceptionsDualFrequency(MeasurementCombinationFactory.getWideLaneCombination(system));
147     }
148 
149     @Test
150     public void testExceptionsNarrowLane() {
151         doTestExceptionsDualFrequency(MeasurementCombinationFactory.getNarrowLaneCombination(system));
152     }
153 
154     @Test
155     public void testExceptionsPhaseMinusCode() {
156         doTestExceptionsSingleFrequency(MeasurementCombinationFactory.getPhaseMinusCodeCombination(system));
157     }
158 
159     @Test
160     public void testExceptionsGRAPHIC() {
161         doTestExceptionsSingleFrequency(MeasurementCombinationFactory.getGRAPHICCombination(system));
162     }
163 
164     private void doTestExceptionsSingleFrequency(final AbstractSingleFrequencyCombination combination) {
165         // Test INCOMPATIBLE_FREQUENCIES_FOR_COMBINATION_OF_MEASUREMENTS exception
166         try {
167             final ObservationData observation = new ObservationData(PredefinedObservationType.L5, 12345678.0, 0, 0);
168             combination.combine(obs1, observation);
169             Assertions.fail("an exception should have been thrown");
170         } catch (OrekitException oe) {
171             Assertions.assertEquals(OrekitMessages.INCOMPATIBLE_FREQUENCIES_FOR_COMBINATION_OF_MEASUREMENTS, oe.getSpecifier());
172         }
173 
174         // Test INVALID_MEASUREMENT_TYPES_FOR_COMBINATION_OF_MEASUREMENTS exception
175         try {
176             final ObservationData observation = new ObservationData(PredefinedObservationType.L1, 12345678.0, 0, 0);
177             combination.combine(obs1, observation);
178             Assertions.fail("an exception should have been thrown");
179         } catch (OrekitException oe) {
180             Assertions.assertEquals(OrekitMessages.INVALID_MEASUREMENT_TYPES_FOR_COMBINATION_OF_MEASUREMENTS, oe.getSpecifier());
181         }
182     }
183 
184     /**
185      * Test exceptions.
186      */
187     private void doTestExceptionsDualFrequency(final AbstractDualFrequencyCombination combination) {
188         // Test INCOMPATIBLE_FREQUENCIES_FOR_COMBINATION_OF_MEASUREMENTS exception
189         try {
190             final ObservationData observation = new ObservationData(PredefinedObservationType.L1, 12345678.0, 0, 0);
191             combination.combine(obs1, observation);
192             Assertions.fail("an exception should have been thrown");
193         } catch (OrekitException oe) {
194             Assertions.assertEquals(OrekitMessages.INCOMPATIBLE_FREQUENCIES_FOR_COMBINATION_OF_MEASUREMENTS, oe.getSpecifier());
195         }
196 
197         // Test INVALID_MEASUREMENT_TYPES_FOR_COMBINATION_OF_MEASUREMENTS exception
198         try {
199             final ObservationData observation = new ObservationData(PredefinedObservationType.D2, 12345678.0, 0, 0);
200             combination.combine(obs1, observation);
201             Assertions.fail("an exception should have been thrown");
202         } catch (OrekitException oe) {
203             Assertions.assertEquals(OrekitMessages.INVALID_MEASUREMENT_TYPES_FOR_COMBINATION_OF_MEASUREMENTS, oe.getSpecifier());
204         }
205     }
206 
207     @Test
208     public void testRinex2GeometryFree() {
209         doTestRinexDualFrequency(MeasurementCombinationFactory.getGeometryFreeCombination(system),
210                      CombinationType.GEOMETRY_FREE, 6.953, 27534453.519,0.0,  Double.NaN, 2, 2);
211     }
212 
213     @Test
214     public void testRinex2IonoFree() {
215         doTestRinexDualFrequency(MeasurementCombinationFactory.getIonosphereFreeCombination(system),
216                      CombinationType.IONO_FREE, 23732467.5026, 3772223175.669, 0.0, 4658 * GnssSignal.F0, 2, 2);
217     }
218 
219     @Test
220     public void testRinex2WideLane() {
221         doTestRinexDualFrequency(MeasurementCombinationFactory.getWideLaneCombination(system),
222                      CombinationType.WIDE_LANE, 23732453.7100, 27534453.519, 0.0, 34 * GnssSignal.F0, 2, 2);
223     }
224 
225     @Test
226     public void testRinex2NarrowLane() {
227         doTestRinexDualFrequency(MeasurementCombinationFactory.getNarrowLaneCombination(system),
228                      CombinationType.NARROW_LANE, 23732481.2951, 221895659.955, 0.0, 274 * GnssSignal.F0, 2, 2);
229     }
230 
231     @Test
232     public void testRinex2MelbourneWubbena() {
233         doTestRinexDualFrequency(MeasurementCombinationFactory.getMelbourneWubbenaCombination(system),
234                      CombinationType.MELBOURNE_WUBBENA, 0.0, 0.0, 3801972.2239, 34 * GnssSignal.F0, 1, 2);
235     }
236 
237     @Test
238     public void testRinex2PhaseMinusCode() {
239         doTestRinex2SingleFrequency(MeasurementCombinationFactory.getPhaseMinusCodeCombination(system),
240                                     CombinationType.PHASE_MINUS_CODE, 73448118.300);
241     }
242 
243     @Test
244     public void testRinex2GRAPHIC() {
245         doTestRinex2SingleFrequency(MeasurementCombinationFactory.getGRAPHICCombination(system),
246                                     CombinationType.GRAPHIC, 60456544.068);
247     }
248 
249     private void doTestRinex2SingleFrequency(final MeasurementCombination combination, final CombinationType type,
250                                              final double expectedL2P2) {
251         // Perform combination on the observation data set depending the Rinex version
252         final CombinedObservationDataSet combinedDataSet = combination.combine(dataSetRinex2);
253         checkCombinedDataSet(combinedDataSet, 1);
254         Assertions.assertEquals(type.getName(), combination.getName());
255         // Verify the combined observation data
256         final List<CombinedObservationData> data = combinedDataSet.getObservationData();
257         // L2/P2
258         Assertions.assertEquals(expectedL2P2,       data.get(0).getValue(),                eps);
259         Assertions.assertEquals(120 * GnssSignal.F0, data.get(0).getCombinedFrequency(), eps);
260 
261     }
262 
263     @Test
264     public void testRinex3GeometryFree() {
265         doTestRinexDualFrequency(MeasurementCombinationFactory.getGeometryFreeCombination(system),
266                      CombinationType.GEOMETRY_FREE, 2.187, 3821708.096, 0.0, Double.NaN, 2, 3);
267     }
268 
269     @Test
270     public void testRinex3IonoFree() {
271         doTestRinexDualFrequency(MeasurementCombinationFactory.getIonosphereFreeCombination(system),
272                      CombinationType.IONO_FREE, 22399214.1934, 179620369.206, 0.0, 235 * GnssSignal.F0, 2, 3);
273     }
274 
275     @Test
276     public void testRinex3WideLane() {
277         doTestRinexDualFrequency(MeasurementCombinationFactory.getWideLaneCombination(system),
278                      CombinationType.WIDE_LANE, 22399239.8790, 3821708.096, 0.0, 5 * GnssSignal.F0, 2, 3);
279     }
280 
281     @Test
282     public void testRinex3NarrowLane() {
283         doTestRinexDualFrequency(MeasurementCombinationFactory.getNarrowLaneCombination(system),
284                     CombinationType.NARROW_LANE, 22399188.5078, 179620457.900, 0.0, 235 * GnssSignal.F0, 2, 3);
285     }
286 
287     @Test
288     public void testRinex3MelbourneWubbena() {
289         doTestRinexDualFrequency(MeasurementCombinationFactory.getMelbourneWubbenaCombination(system),
290                      CombinationType.MELBOURNE_WUBBENA, 0.0, 0.0, -18577480.4117, 5 * GnssSignal.F0, 1, 3);
291     }
292 
293     @Test
294     public void testRinex3PhaseMinusCode() {
295         doTestRinex3SingleFrequency(MeasurementCombinationFactory.getPhaseMinusCodeCombination(system),
296                                     CombinationType.PHASE_MINUS_CODE, 95309391.697, 69321899.401,
297                                     69321893.420, 65500187.511);
298     }
299 
300     @Test
301     public void testRinex3GRAPHIC() {
302         doTestRinex3SingleFrequency(MeasurementCombinationFactory.getGRAPHICCombination(system),
303                                     CombinationType.GRAPHIC, 70053877.7315, 57060139.2905,
304                                     57060136.2880, 55149281.1465);
305     }
306 
307     private void doTestRinex3SingleFrequency(final MeasurementCombination combination, final CombinationType type,
308                                              final double expected1C, final double expected2W,
309                                              final double expected2X, final double expected5X) {
310         // Perform combination on the observation data set depending the Rinex version
311         final CombinedObservationDataSet combinedDataSet = combination.combine(dataSetRinex3);
312         Assertions.assertEquals(type.getName(), combination.getName());
313         // Verify the combined observation data
314         final List<CombinedObservationData> data = combinedDataSet.getObservationData();
315         // L1C/C1C
316         Assertions.assertEquals(expected1C,          data.get(0).getValue(),                eps);
317         Assertions.assertEquals(154 * GnssSignal.F0, data.get(0).getCombinedFrequency(), eps);
318         // L2W/C2W
319         Assertions.assertEquals(expected2W,          data.get(1).getValue(),                eps);
320         Assertions.assertEquals(120 * GnssSignal.F0, data.get(1).getCombinedFrequency(), eps);
321         // L2X/C2X
322         Assertions.assertEquals(expected2X,          data.get(2).getValue(),                eps);
323         Assertions.assertEquals(120 * GnssSignal.F0, data.get(1).getCombinedFrequency(), eps);
324         // L5X/C5X
325         Assertions.assertEquals(expected5X,          data.get(3).getValue(),                eps);
326         Assertions.assertEquals(115 * GnssSignal.F0, data.get(3).getCombinedFrequency(), eps);
327     }
328 
329     /**
330      * Test if Rinex formats can be used for the combination of measurements
331      */
332     private void doTestRinexDualFrequency(final MeasurementCombination combination, final CombinationType expectedType,
333                              final double expectedRangeValue, final double expectedPhaseValue, final double expectedRangePhase,
334                              final double expectedFrequency, final int expectedSize, final int rinexVersion) {
335 
336         // Perform combination on the observation data set depending the Rinex version
337         final CombinedObservationDataSet combinedDataSet;
338         if (rinexVersion == 2) {
339             combinedDataSet = combination.combine(dataSetRinex2);
340             checkCombinedDataSet(combinedDataSet, expectedSize);
341         } else {
342             combinedDataSet = combination.combine(dataSetRinex3);
343             Assertions.assertEquals(expectedSize, combinedDataSet.getObservationData().size());
344         }
345 
346         Assertions.assertEquals(expectedType.getName(), combination.getName());
347 
348         // Verify the combined observation data
349         for (CombinedObservationData cod : combinedDataSet.getObservationData()) {
350 
351             if (cod.getMeasurementType() == MeasurementType.CARRIER_PHASE) {
352 
353                 Assertions.assertEquals(expectedPhaseValue, cod.getValue(),                eps);
354                 Assertions.assertEquals(expectedFrequency,  cod.getCombinedFrequency(), eps);
355                 Assertions.assertEquals(expectedType,       cod.getCombinationType());
356 
357             } else if (cod.getMeasurementType() == MeasurementType.PSEUDO_RANGE) {
358 
359                 Assertions.assertEquals(expectedRangeValue, cod.getValue(),                eps);
360                 Assertions.assertEquals(expectedFrequency,  cod.getCombinedFrequency(), eps);
361                 Assertions.assertEquals(expectedType,       cod.getCombinationType());
362 
363             } else if (cod.getMeasurementType() == MeasurementType.COMBINED_RANGE_PHASE) {
364 
365                 Assertions.assertEquals(expectedRangePhase, cod.getValue(),                eps);
366                 Assertions.assertEquals(expectedFrequency,  cod.getCombinedFrequency(), eps);
367                 Assertions.assertEquals(expectedType,       cod.getCombinationType());
368 
369             }
370 
371         }
372     }
373 
374     private void checkCombinedDataSet(final CombinedObservationDataSet combinedDataSet,
375                                       final int expectedSize) {
376         // Verify the number of combined data set
377         Assertions.assertEquals(expectedSize, combinedDataSet.getObservationData().size());
378         // Verify satellite data
379         Assertions.assertEquals(30, combinedDataSet.getPrnNumber());
380         Assertions.assertEquals(SatelliteSystem.GPS, combinedDataSet.getSatelliteSystem());
381         // Verify receiver clock
382         Assertions.assertEquals(0.0, combinedDataSet.getRcvrClkOffset(), eps);
383         // Verify date
384         Assertions.assertEquals("2016-02-13T00:49:43.000Z", combinedDataSet.getDate().toString());
385     }
386 
387     @Test
388     public void testIssue746() {
389 
390         // This test uses the example provided by Amir Allahvirdi-Zadeh in the Orekit issue tracker
391         // Source of the values: https://gitlab.orekit.org/orekit/orekit/-/issues/746
392 
393         // Build the observation data
394         final ObservationData obs1 = new ObservationData(PredefinedObservationType.L1, 1.17452520667E8, 0, 0);
395         final ObservationData obs2 = new ObservationData(PredefinedObservationType.L2, 9.1521434853E7, 0, 0);
396 
397         // Ionosphere-free measurement
398         final IonosphereFreeCombination ionoFree = MeasurementCombinationFactory.getIonosphereFreeCombination(SatelliteSystem.GPS);
399         final CombinedObservationData   combined = ionoFree.combine(obs1, obs2);
400 
401         // Combine data
402         final double wavelength         = Constants.SPEED_OF_LIGHT / combined.getCombinedFrequency();
403         final double combineValueMeters = combined.getValue() * wavelength;
404 
405         // Verify
406         Assertions.assertEquals(22350475.245, combineValueMeters, 0.001);
407 
408     }
409 
410 }