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.bodies;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.util.HashSet;
22  import java.util.Locale;
23  import java.util.Scanner;
24  import java.util.Set;
25  
26  import org.hamcrest.MatcherAssert;
27  import org.hamcrest.Matchers;
28  import org.hipparchus.geometry.euclidean.threed.Vector3D;
29  import org.junit.jupiter.api.Assertions;
30  import org.junit.jupiter.api.BeforeEach;
31  import org.junit.jupiter.api.Test;
32  import org.junit.jupiter.params.ParameterizedTest;
33  import org.junit.jupiter.params.provider.EnumSource;
34  import org.orekit.Utils;
35  import org.orekit.data.DataContext;
36  import org.orekit.errors.OrekitException;
37  import org.orekit.frames.Frame;
38  import org.orekit.frames.FramesFactory;
39  import org.orekit.time.AbsoluteDate;
40  import org.orekit.time.TimeScale;
41  import org.orekit.time.TimeScalesFactory;
42  import org.orekit.utils.Constants;
43  import org.orekit.utils.PVCoordinates;
44  
45  public class JPLEphemeridesLoaderTest {
46  
47      @Test
48      void testConstantsJPL() {
49          Utils.setDataRoot("regular-data/de405-ephemerides");
50  
51          JPLEphemeridesLoader loader =
52              new JPLEphemeridesLoader(JPLEphemeridesLoader.DEFAULT_DE_SUPPORTED_NAMES,
53                                       JPLEphemeridesLoader.EphemerisType.SUN);
54          Assertions.assertEquals(149597870691.0, loader.getLoadedAstronomicalUnit(), 0.1);
55          Assertions.assertEquals(81.30056, loader.getLoadedEarthMoonMassRatio(), 1.0e-8);
56          Assertions.assertTrue(Double.isNaN(loader.getLoadedConstant("not-a-constant")));
57      }
58  
59      @Test
60      void testConstantsInpop() {
61          Utils.setDataRoot("inpop");
62          JPLEphemeridesLoader loader =
63              new JPLEphemeridesLoader(JPLEphemeridesLoader.DEFAULT_INPOP_SUPPORTED_NAMES,
64                                       JPLEphemeridesLoader.EphemerisType.SUN);
65          Assertions.assertEquals(149597870691.0, loader.getLoadedAstronomicalUnit(), 0.1);
66          Assertions.assertEquals(81.30057, loader.getLoadedEarthMoonMassRatio(), 1.0e-8);
67      }
68  
69      @Test
70      void testGMJPL() {
71          Utils.setDataRoot("regular-data/de405-ephemerides");
72  
73          JPLEphemeridesLoader loader =
74              new JPLEphemeridesLoader(JPLEphemeridesLoader.DEFAULT_DE_SUPPORTED_NAMES,
75                                       JPLEphemeridesLoader.EphemerisType.SUN);
76          Assertions.assertEquals(22032.080e9,
77                              loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.MERCURY),
78                              1.0e6);
79          Assertions.assertEquals(324858.599e9,
80                              loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.VENUS),
81                              1.0e6);
82          Assertions.assertEquals(42828.314e9,
83                              loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.MARS),
84                              1.0e6);
85          Assertions.assertEquals(126712767.863e9,
86                              loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.JUPITER),
87                              6.0e7);
88          Assertions.assertEquals(37940626.063e9,
89                              loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.SATURN),
90                              2.0e6);
91          Assertions.assertEquals(5794549.007e9,
92                              loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.URANUS),
93                              1.0e6);
94          Assertions.assertEquals(6836534.064e9,
95                              loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.NEPTUNE),
96                              1.0e6);
97          Assertions.assertEquals(981.601e9,
98                              loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.PLUTO),
99                              1.0e6);
100         Assertions.assertEquals(132712440017.987e9,
101                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.SUN),
102                             1.0e6);
103         Assertions.assertEquals(4902.801e9,
104                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.MOON),
105                             1.0e6);
106         Assertions.assertEquals(403503.233e9,
107                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.EARTH_MOON),
108                             1.0e6);
109     }
110 
111     @Test
112     void testGMInpop() {
113 
114         Utils.setDataRoot("inpop");
115 
116         JPLEphemeridesLoader loader =
117                 new JPLEphemeridesLoader("^inpop.*TCB.*littleendian.*\\.dat$",
118                                          JPLEphemeridesLoader.EphemerisType.SUN);
119         Assertions.assertEquals(22032.081e9,
120                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.MERCURY),
121                             1.0e6);
122         Assertions.assertEquals(324858.597e9,
123                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.VENUS),
124                             1.0e6);
125         Assertions.assertEquals(42828.376e9,
126                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.MARS),
127                             1.0e6);
128         Assertions.assertEquals(126712764.535e9,
129                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.JUPITER),
130                             6.0e7);
131         Assertions.assertEquals(37940585.443e9,
132                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.SATURN),
133                             2.0e6);
134         Assertions.assertEquals(5794549.099e9,
135                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.URANUS),
136                             1.0e6);
137         Assertions.assertEquals(6836527.128e9,
138                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.NEPTUNE),
139                             1.0e6);
140         Assertions.assertEquals(971.114e9,
141                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.PLUTO),
142                             1.0e6);
143         Assertions.assertEquals(132712442110.032e9,
144                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.SUN),
145                             1.0e6);
146         Assertions.assertEquals(4902.800e9,
147                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.MOON),
148                             1.0e6);
149         Assertions.assertEquals(403503.250e9,
150                             loader.getLoadedGravitationalCoefficient(JPLEphemeridesLoader.EphemerisType.EARTH_MOON),
151                             1.0e6);
152     }
153 
154     @Test
155     void testDerivative405() {
156         Utils.setDataRoot("regular-data/de405-ephemerides");
157         checkDerivative(JPLEphemeridesLoader.DEFAULT_DE_SUPPORTED_NAMES,
158                         new AbsoluteDate(1969, 6, 25, TimeScalesFactory.getTT()),
159                         691200.0);
160     }
161 
162     @Test
163     void testDerivative406() {
164         Utils.setDataRoot("regular-data:regular-data/de406-ephemerides");
165         checkDerivative(JPLEphemeridesLoader.DEFAULT_DE_SUPPORTED_NAMES,
166                         new AbsoluteDate(2964, 9, 26, TimeScalesFactory.getTT()),
167                         1382400.0);
168     }
169 
170     @Test
171     void testDummyEarth() {
172         Utils.setDataRoot("regular-data/de405-ephemerides");
173         JPLEphemeridesLoader loader =
174                 new JPLEphemeridesLoader(JPLEphemeridesLoader.DEFAULT_DE_SUPPORTED_NAMES,
175                                          JPLEphemeridesLoader.EphemerisType.EARTH);
176         CelestialBody body = loader.loadCelestialBody(CelestialBodyFactory.EARTH);
177         AbsoluteDate date = new AbsoluteDate(1950, 1, 12, TimeScalesFactory.getTT());
178         Frame eme2000 = FramesFactory.getEME2000();
179         for (double h = 0; h < 86400; h += 60.0) {
180             PVCoordinates pv = body.getPVCoordinates(date, eme2000);
181             Assertions.assertEquals(0, pv.getPosition().getNorm(), 1.0e-15);
182             Assertions.assertEquals(0, pv.getVelocity().getNorm(), 1.0e-15);
183         }
184     }
185 
186     @Test
187     void testEndianness() {
188         Utils.setDataRoot("inpop");
189         JPLEphemeridesLoader.EphemerisType type = JPLEphemeridesLoader.EphemerisType.MARS;
190         JPLEphemeridesLoader loaderInpopTCBBig =
191                 new JPLEphemeridesLoader("^inpop.*_TCB_.*_bigendian\\.dat$", type);
192         CelestialBody bodysInpopTCBBig = loaderInpopTCBBig.loadCelestialBody(CelestialBodyFactory.MARS);
193         Assertions.assertEquals(1.0, loaderInpopTCBBig.getLoadedConstant("TIMESC"), 1.0e-10);
194         JPLEphemeridesLoader loaderInpopTCBLittle =
195                 new JPLEphemeridesLoader("^inpop.*_TCB_.*_littleendian\\.dat$", type);
196         CelestialBody bodysInpopTCBLittle = loaderInpopTCBLittle.loadCelestialBody(CelestialBodyFactory.MARS);
197         Assertions.assertEquals(1.0, loaderInpopTCBLittle.getLoadedConstant("TIMESC"), 1.0e-10);
198         AbsoluteDate t0 = new AbsoluteDate(1969, 7, 17, 10, 43, 23.4, TimeScalesFactory.getTT());
199         Frame eme2000   = FramesFactory.getEME2000();
200         for (double dt = 0; dt < 30 * Constants.JULIAN_DAY; dt += 3600) {
201             AbsoluteDate date        = t0.shiftedBy(dt);
202             Vector3D pInpopTCBBig    = bodysInpopTCBBig.getPosition(date, eme2000);
203             Vector3D pInpopTCBLittle = bodysInpopTCBLittle.getPosition(date, eme2000);
204             Assertions.assertEquals(0.0, pInpopTCBBig.distance(pInpopTCBLittle), 1.0e-10);
205         }
206         for (String name : DataContext.getDefault().getDataProvidersManager().getLoadedDataNames()) {
207             Assertions.assertTrue(name.contains("inpop"));
208         }
209     }
210 
211     @Test
212     void testInpopvsJPL() {
213         Utils.setDataRoot("regular-data:inpop");
214         JPLEphemeridesLoader.EphemerisType type = JPLEphemeridesLoader.EphemerisType.MARS;
215         JPLEphemeridesLoader loaderDE405 =
216                 new JPLEphemeridesLoader("^unxp(\\d\\d\\d\\d)\\.405$", type);
217         CelestialBody bodysDE405 = loaderDE405.loadCelestialBody(CelestialBodyFactory.MARS);
218         JPLEphemeridesLoader loaderInpopTDBBig =
219                 new JPLEphemeridesLoader("^inpop.*_TDB_.*_bigendian\\.dat$", type);
220         CelestialBody bodysInpopTDBBig = loaderInpopTDBBig.loadCelestialBody(CelestialBodyFactory.MARS);
221         Assertions.assertEquals(0.0, loaderInpopTDBBig.getLoadedConstant("TIMESC"), 1.0e-10);
222         JPLEphemeridesLoader loaderInpopTCBBig =
223                 new JPLEphemeridesLoader("^inpop.*_TCB_.*_bigendian\\.dat$", type);
224         CelestialBody bodysInpopTCBBig = loaderInpopTCBBig.loadCelestialBody(CelestialBodyFactory.MARS);
225         Assertions.assertEquals(1.0, loaderInpopTCBBig.getLoadedConstant("TIMESC"), 1.0e-10);
226         AbsoluteDate t0 = new AbsoluteDate(1969, 7, 17, 10, 43, 23.4, TimeScalesFactory.getTT());
227         Frame eme2000   = FramesFactory.getEME2000();
228         for (double dt = 0; dt < 30 * Constants.JULIAN_DAY; dt += 3600) {
229             AbsoluteDate date = t0.shiftedBy(dt);
230             Vector3D pDE405          = bodysDE405.getPosition(date, eme2000);
231             Vector3D pInpopTDBBig    = bodysInpopTDBBig.getPosition(date, eme2000);
232             Vector3D pInpopTCBBig    = bodysInpopTCBBig.getPosition(date, eme2000);
233             Assertions.assertTrue(pDE405.distance(pInpopTDBBig) >  650.0);
234             Assertions.assertTrue(pDE405.distance(pInpopTDBBig) < 1050.0);
235             Assertions.assertTrue(pDE405.distance(pInpopTCBBig) > 1000.0);
236             Assertions.assertTrue(pDE405.distance(pInpopTCBBig) < 2000.0);
237         }
238 
239     }
240 
241     @Test
242     void testOverlappingEphemeridesData() throws IOException {
243         Utils.setDataRoot("overlapping-data/data.zip");
244 
245         // the data root contains two ephemerides files (JPL DE 405), which overlap in the period
246         // (1999-12-23T23:58:55.816, 2000-01-24T23:58:55.815)
247         // this test checks that the data in the overlapping and surrounding range is loaded correctly
248         // from both files (see issue #113).
249 
250         // as the bug only manifests if the DataLoader first loads the ephemerides file containing earlier
251         // data points, the data files are zipped to get a deterministic order when listing files
252 
253         CelestialBody moon = CelestialBodyFactory.getMoon();
254 
255         // 1999/12/31 0h00
256         final AbsoluteDate initDate = new AbsoluteDate(1999, 12, 31, 00, 00, 00, TimeScalesFactory.getUTC());
257         moon.getPVCoordinates(initDate, FramesFactory.getGCRF());
258 
259         // 2000/04/01 0h00
260         final AbsoluteDate otherDate = new AbsoluteDate(2000, 02, 01, 00, 00, 00, TimeScalesFactory.getUTC());
261         moon.getPVCoordinates(otherDate, FramesFactory.getGCRF());
262 
263         // 3 years from initDate
264         AbsoluteDate currentDate = new AbsoluteDate(1999, 12, 01, 00, 00, 00, TimeScalesFactory.getTAI());
265         AbsoluteDate finalDate = new AbsoluteDate(2000, 03, 14, 00, 00, 00, TimeScalesFactory.getTAI());
266 
267         while (currentDate.compareTo(finalDate) < 0)  {
268             currentDate = currentDate.shiftedBy(Constants.JULIAN_DAY);
269             moon.getPVCoordinates(currentDate, FramesFactory.getGCRF());
270         }
271 
272     }
273 
274     /**
275      * Check against subset of DE431 validation set from
276      * https://ssd.jpl.nasa.gov/ftp/eph/planets/Linux/de431/testpo.431
277      * @throws IOException on error.
278      */
279     @Test
280     public void testPo431() throws IOException {
281         // setup
282         Utils.setDataRoot("regular-data/de431-ephemerides");
283         final int nChecked = testPo("/bodies/testpo.431");
284         MatcherAssert.assertThat(nChecked, Matchers.is(5));
285     }
286 
287     /**
288      * Check against subset of DE405 validation set from
289      * https://ssd.jpl.nasa.gov/ftp/eph/planets/Linux/de405/testpo.405
290      * @throws IOException on error.
291      */
292     @Test
293     public void testPo405() throws IOException {
294         Utils.setDataRoot("regular-data/de405-ephemerides");
295         final int nChecked = testPo("/bodies/testpo.405");
296         MatcherAssert.assertThat(nChecked, Matchers.is(5));
297     }
298 
299     /**
300      * Check against subset of DE440 validation set from
301      * https://ssd.jpl.nasa.gov/ftp/eph/planets/Linux/de440/testpo.440
302      * @throws IOException on error.
303      */
304     @Test
305     public void testPo440() throws IOException {
306         Utils.setDataRoot("2007");
307         final int nChecked = testPo("/2007/testpo.440");
308         MatcherAssert.assertThat(nChecked, Matchers.is(11));
309     }
310 
311     /**
312      * Test parsing of 2021 format against a truncated version of de440-ephemerides.
313      *
314      * @param ephemerisType type of ephemeris to load.
315      * @see <a href="https://gitlab.orekit.org/orekit/orekit/-/work_items/1938">Issue 1938</a>
316      */
317     @ParameterizedTest
318     @EnumSource(JPLEphemeridesLoader.EphemerisType.class)
319     public void Test2021FormatParsing(final JPLEphemeridesLoader.EphemerisType ephemerisType) {
320 
321         // GIVEN
322         Utils.setDataRoot("regular-data/de440-ephemerides");
323 
324         // WHEN
325         JPLEphemeridesLoader loaderWithout2021Format =
326                         new JPLEphemeridesLoader(JPLEphemeridesLoader.DEFAULT_DE_SUPPORTED_NAMES, ephemerisType);
327 
328         JPLEphemeridesLoader loaderWith2021Format =
329                         new JPLEphemeridesLoader(JPLEphemeridesLoader.DEFAULT_DE_2021_SUPPORTED_NAMES, ephemerisType);
330 
331         // THEN
332         Assertions.assertThrows(OrekitException.class, loaderWithout2021Format::getLoadedAstronomicalUnit);
333         Assertions.assertDoesNotThrow(loaderWith2021Format::getLoadedAstronomicalUnit);
334     }
335 
336     private int testPo(String name) throws IOException {
337         JPLEphemeridesLoader loader = new JPLEphemeridesLoader(
338                 JPLEphemeridesLoader.DEFAULT_DE_SUPPORTED_NAMES,
339                 JPLEphemeridesLoader.EphemerisType.SUN);
340         final double au = loader.getLoadedAstronomicalUnit();
341         final double day = Constants.JULIAN_DAY;
342         final TimeScale tdb = TimeScalesFactory.getTDB();
343         final double tolS = 1e-13;
344         final double tolP = 1e-13 * au; // tolerance used by JPL
345         final double tolV = 1e-13 * au / day; // tolerance used by JPL
346         final Frame gcrf = FramesFactory.getGCRF();
347         final Frame icrf = FramesFactory.getICRF();
348         final Set<Integer> nearEarth = new HashSet<>();
349         nearEarth.add(3);
350         nearEarth.add(10);
351         nearEarth.add(13);
352 
353         int i = 0;
354         // 431  1999.12.01 2451513.5  8 11  2      -23.03253618370120000000
355         try(InputStream is = this.getClass().getResourceAsStream(name);
356             final Scanner scanner = new Scanner(is, "UTF-8"))
357         {
358             scanner.useLocale(Locale.ROOT);
359             while (scanner.hasNext()) {
360                 final int version = scanner.nextInt();
361                 final String dateString = scanner.next().replace('.', '-');
362                 final AbsoluteDate date = new AbsoluteDate(dateString, tdb);
363                 final double jd = scanner.nextDouble();
364                 final String message = version + " " + dateString;
365                 MatcherAssert.assertThat(
366                         message,
367                         date.getJD(tdb),
368                         Matchers.closeTo(jd, tolS));
369                 final int targetInt = scanner.nextInt();
370                 final int centerInt = scanner.nextInt();
371                 final String targetName = getName(targetInt);
372                 final String centerName = getName(centerInt);
373                 if (targetName == null || centerName == null) {
374                     scanner.nextLine();
375                     continue;
376                 }
377                 CelestialBody target = CelestialBodyFactory.getBody(targetName);
378                 CelestialBody center = CelestialBodyFactory.getBody(centerName);
379                 Frame frame;
380                 if (nearEarth.contains(targetInt) || nearEarth.contains(centerInt)) {
381                     frame = gcrf;
382                 } else {
383                     frame = icrf;
384                 }
385                 PVCoordinates targetPv = target.getPVCoordinates(date, frame);
386                 PVCoordinates centerPv = center.getPVCoordinates(date, frame);
387                 final int coordinate = scanner.nextInt();
388                 double actual = getCoordinate(targetPv, centerPv, coordinate);
389                 final double expected = scanner.nextDouble();
390                 if (coordinate < 4) {
391                     // position
392                     MatcherAssert.assertThat(
393                             message,
394                             actual,
395                             Matchers.closeTo(expected * au, tolP));
396                 } else {
397                     // velocity
398                     MatcherAssert.assertThat(
399                             message,
400                             actual,
401                             Matchers.closeTo(expected * au / day, tolV));
402                 }
403                 i++;
404                 scanner.nextLine();
405             }
406         }
407         return i;
408     }
409 
410     /**
411      * Get the selected coordinate
412      *
413      * @param targetPv   A
414      * @param centerPv   B
415      * @param coordinate number from JPL.
416      * @return (A - B).coordinate
417      */
418     private double getCoordinate(PVCoordinates targetPv,
419                                  PVCoordinates centerPv,
420                                  int coordinate) {
421         final Vector3D d;
422         if (coordinate < 4) {
423             d = targetPv.getPosition().subtract(centerPv.getPosition());
424         } else {
425             d = targetPv.getVelocity().subtract(centerPv.getVelocity());
426             coordinate -= 3;
427         }
428 
429         switch (coordinate) {
430             case 1:
431                 return d.getX();
432             case 2:
433                 return d.getY();
434             case 3:
435                 return d.getZ();
436             default:
437                 throw new RuntimeException("Unknown coordinate: " + coordinate);
438         }
439     }
440 
441 
442     /**
443      * Get the Orekit body name for a JPL body number.
444      *
445      * @param body number.
446      * @return body name.
447      */
448     private static String getName(int body) {
449         /*
450         target number (1-Mercury, ...,3-Earth, ,,,9-Pluto, 10-Moon, 11-Sun,
451                        12-Solar System Barycenter, 13-Earth-Moon Barycenter
452                        14-Nutations, 15-Librations)
453          */
454         switch (body) {
455             case 1:
456                 return CelestialBodyFactory.MERCURY;
457             case 2:
458                 return CelestialBodyFactory.VENUS;
459             case 3:
460                 return CelestialBodyFactory.EARTH;
461             case 4:
462                 return CelestialBodyFactory.MARS;
463             case 5:
464                 return CelestialBodyFactory.JUPITER;
465             case 6:
466                 return CelestialBodyFactory.SATURN;
467             case 7:
468                 return CelestialBodyFactory.URANUS;
469             case 8:
470                 return CelestialBodyFactory.NEPTUNE;
471             case 9:
472                 return CelestialBodyFactory.PLUTO;
473             case 10:
474                 return CelestialBodyFactory.MOON;
475             case 11:
476                 return CelestialBodyFactory.SUN;
477             case 12:
478                 return CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER;
479             case 13:
480                 return CelestialBodyFactory.EARTH_MOON;
481             default:
482                 // nutations and librations not implemented
483                 return null;
484         }
485     }
486 
487     private void checkDerivative(String supportedNames, AbsoluteDate date, double maxChunkDuration)
488         {
489         JPLEphemeridesLoader loader =
490             new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.MERCURY);
491         CelestialBody body = loader.loadCelestialBody(CelestialBodyFactory.MERCURY);
492         double h = 20;
493 
494         // eight points finite differences estimation of the velocity
495         Frame eme2000 = FramesFactory.getEME2000();
496         Vector3D pm4h = body.getPosition(date.shiftedBy(-4 * h), eme2000);
497         Vector3D pm3h = body.getPosition(date.shiftedBy(-3 * h), eme2000);
498         Vector3D pm2h = body.getPosition(date.shiftedBy(-2 * h), eme2000);
499         Vector3D pm1h = body.getPosition(date.shiftedBy(    -h), eme2000);
500         Vector3D pp1h = body.getPosition(date.shiftedBy(     h), eme2000);
501         Vector3D pp2h = body.getPosition(date.shiftedBy( 2 * h), eme2000);
502         Vector3D pp3h = body.getPosition(date.shiftedBy( 3 * h), eme2000);
503         Vector3D pp4h = body.getPosition(date.shiftedBy( 4 * h), eme2000);
504         Vector3D d4   = pp4h.subtract(pm4h);
505         Vector3D d3   = pp3h.subtract(pm3h);
506         Vector3D d2   = pp2h.subtract(pm2h);
507         Vector3D d1   = pp1h.subtract(pm1h);
508         double c = 1.0 / (840 * h);
509         Vector3D estimatedV = new Vector3D(-3 * c, d4, 32 * c, d3, -168 * c, d2, 672 * c, d1);
510 
511         Vector3D loadedV = body.getPVCoordinates(date, eme2000).getVelocity();
512         Assertions.assertEquals(0, loadedV.subtract(estimatedV).getNorm(), 3.5e-10 * loadedV.getNorm());
513         Assertions.assertEquals(maxChunkDuration, loader.getMaxChunksDuration(), 1.0e-10);
514     }
515 
516     @BeforeEach
517     public void setUp() {
518         Utils.setDataRoot("regular-data");
519     }
520 
521 }