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.models.earth.atmosphere.data;
18  
19  import org.hipparchus.ode.ODEIntegrator;
20  import org.hipparchus.ode.nonstiff.ClassicalRungeKuttaIntegrator;
21  import org.hipparchus.util.FastMath;
22  import org.junit.jupiter.api.AfterEach;
23  import org.junit.jupiter.api.Assertions;
24  import org.junit.jupiter.api.BeforeEach;
25  import org.junit.jupiter.api.DisplayName;
26  import org.junit.jupiter.api.RepeatedTest;
27  import org.junit.jupiter.api.Test;
28  import org.orekit.Utils;
29  import org.orekit.bodies.CelestialBody;
30  import org.orekit.bodies.CelestialBodyFactory;
31  import org.orekit.bodies.OneAxisEllipsoid;
32  import org.orekit.data.DataSource;
33  import org.orekit.errors.OrekitException;
34  import org.orekit.forces.drag.DragForce;
35  import org.orekit.forces.drag.IsotropicDrag;
36  import org.orekit.frames.Frame;
37  import org.orekit.frames.FramesFactory;
38  import org.orekit.models.earth.atmosphere.Atmosphere;
39  import org.orekit.models.earth.atmosphere.DTM2000;
40  import org.orekit.models.earth.atmosphere.DTM2000InputParameters;
41  import org.orekit.models.earth.atmosphere.NRLMSISE00;
42  import org.orekit.models.earth.atmosphere.NRLMSISE00InputParameters;
43  import org.orekit.models.earth.atmosphere.data.MarshallSolarActivityFutureEstimation.StrengthLevel;
44  import org.orekit.orbits.KeplerianOrbit;
45  import org.orekit.orbits.Orbit;
46  import org.orekit.orbits.OrbitType;
47  import org.orekit.orbits.PositionAngleType;
48  import org.orekit.propagation.SpacecraftState;
49  import org.orekit.propagation.numerical.NumericalPropagator;
50  import org.orekit.propagation.sampling.OrekitStepHandler;
51  import org.orekit.time.AbsoluteDate;
52  import org.orekit.time.DateComponents;
53  import org.orekit.time.Month;
54  import org.orekit.time.TimeScale;
55  import org.orekit.time.TimeScalesFactory;
56  import org.orekit.time.TimeStampedDouble;
57  import org.orekit.utils.Constants;
58  import org.orekit.utils.GenericTimeStampedCache;
59  import org.orekit.utils.IERSConventions;
60  
61  import java.net.URISyntaxException;
62  import java.net.URL;
63  import java.util.ArrayList;
64  import java.util.Comparator;
65  import java.util.List;
66  import java.util.concurrent.Callable;
67  import java.util.concurrent.ExecutorService;
68  import java.util.concurrent.Executors;
69  import java.util.concurrent.TimeUnit;
70  import java.util.concurrent.atomic.AtomicReference;
71  import java.util.stream.Collectors;
72  
73  import static org.hamcrest.MatcherAssert.assertThat;
74  import static org.orekit.OrekitMatchers.closeTo;
75  import static org.orekit.OrekitMatchers.pvCloseTo;
76  
77  public class MarshallSolarActivityFutureEstimationTest {
78  
79      /**
80       * Check {@link MarshallSolarActivityFutureEstimation#get24HoursKp(AbsoluteDate)} and
81       * {@link MarshallSolarActivityFutureEstimation#getThreeHourlyKP(AbsoluteDate)} are
82       * continuous.
83       */
84      @Test
85      public void testGetKp() {
86          //setup
87          DTM2000InputParameters flux = getFlux();
88          final AbsoluteDate july = new AbsoluteDate(2008, 7, 1, utc);
89          final AbsoluteDate august = new AbsoluteDate(2008, 8, 1, utc);
90          final AbsoluteDate middle = july.shiftedBy(august.durationFrom(july) / 2.0);
91          final double minute = 60;
92          final AbsoluteDate before = middle.shiftedBy(-minute);
93          final AbsoluteDate after = middle.shiftedBy(+minute);
94  
95          // action + verify
96          // non-chaotic i.e. small change in input produces small change in output.
97          double kpHourlyDifference =
98                  flux.getThreeHourlyKP(before) - flux.getThreeHourlyKP(after);
99          assertThat(kpHourlyDifference, closeTo(0.0, 1e-4));
100         double kpDailyDifference = flux.get24HoursKp(before) - flux.get24HoursKp(after);
101         assertThat(kpDailyDifference, closeTo(0.0, 1e-4));
102         assertThat(flux.getThreeHourlyKP(middle), closeTo(2.18, 0.3));
103         assertThat(flux.get24HoursKp(middle), closeTo(2.18, 0.3));
104     }
105 
106     /**
107      * Check {@link MarshallSolarActivityFutureEstimation#getAp(AbsoluteDate)} is
108      * continuous.
109      */
110     @Test
111     public void testGetAp() {
112         //setup
113         NRLMSISE00InputParameters flux = getFlux();
114         final AbsoluteDate july = new AbsoluteDate(2008, 7, 1, utc);
115         final AbsoluteDate august = new AbsoluteDate(2008, 8, 1, utc);
116         final AbsoluteDate middle = july.shiftedBy(august.durationFrom(july) / 2.0);
117         final double minute = 60;
118         final AbsoluteDate before = middle.shiftedBy(-minute);
119         final AbsoluteDate after = middle.shiftedBy(+minute);
120 
121         // action + verify
122         // non-chaotic i.e. small change in input produces small change in output.
123         final double[] apBefore = flux.getAp(before);
124         final double[] apAfter  = flux.getAp(after);
125         for (int i = 0; i < apBefore.length; i++) {
126             assertThat(apBefore[i] - apAfter[i], closeTo(0.0, 1e-4));
127         }
128         for (double ap: flux.getAp(middle)) {
129             assertThat(ap, closeTo(8.0, 1e-10));
130         }
131     }
132 
133     /**
134      * Check integration error is small when integrating the same equations over the same
135      * interval.
136      */
137     @Test
138     public void testWithPropagator() {
139         CelestialBody sun = CelestialBodyFactory.getSun();
140         final Frame eci = FramesFactory.getGCRF();
141         final Frame ecef = FramesFactory.getITRF(IERSConventions.IERS_2010, true);
142         AbsoluteDate date = new AbsoluteDate(2004, 1, 1, utc);
143         OneAxisEllipsoid earth = new OneAxisEllipsoid(
144                 Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
145                 Constants.WGS84_EARTH_FLATTENING,
146                 ecef);
147         Orbit orbit = new KeplerianOrbit(
148                 6378137 + 400e3, 1e-3, FastMath.toRadians(50), 0, 0, 0,
149                 PositionAngleType.TRUE, eci, date, Constants.EIGEN5C_EARTH_MU);
150         final SpacecraftState ic = new SpacecraftState(orbit);
151 
152         final AbsoluteDate end = date.shiftedBy(5 * Constants.JULIAN_DAY);
153         final AbsoluteDate resetDate = date.shiftedBy(0.8 * Constants.JULIAN_DAY + 0.1);
154 
155         final SpacecraftState[] lastState = new SpacecraftState[1];
156         final OrekitStepHandler stepSaver = interpolator -> {
157             final AbsoluteDate start = interpolator.getPreviousState().getDate();
158             if (start.compareTo(resetDate) < 0) {
159                 lastState[0] = interpolator.getPreviousState();
160             }
161         };
162 
163         // propagate with state rest to take slightly different path
164         NumericalPropagator propagator = getNumericalPropagatorWithDTM(sun, earth, ic);
165         propagator.setStepHandler(stepSaver);
166         propagator.propagate(resetDate);
167         propagator.resetInitialState(lastState[0]);
168         propagator.clearStepHandlers();
169         SpacecraftState actual = propagator.propagate(end);
170 
171         // propagate straight through
172         propagator = getNumericalPropagatorWithDTM(sun, earth, ic);
173         propagator.resetInitialState(ic);
174         propagator.clearStepHandlers();
175         SpacecraftState expected = propagator.propagate(end);
176 
177         assertThat(actual.getPVCoordinates(), pvCloseTo(expected.getPVCoordinates(), 1.0));
178 
179         // propagate with state rest to take slightly different path
180         propagator = getNumericalPropagatorWithMSIS(sun, earth, ic);
181         propagator.setStepHandler(stepSaver);
182         propagator.propagate(resetDate);
183         propagator.resetInitialState(lastState[0]);
184         propagator.clearStepHandlers();
185         actual = propagator.propagate(end);
186 
187         // propagate straight through
188         propagator = getNumericalPropagatorWithMSIS(sun, earth, ic);
189         propagator.resetInitialState(ic);
190         propagator.clearStepHandlers();
191         expected = propagator.propagate(end);
192 
193         assertThat(actual.getPVCoordinates(), pvCloseTo(expected.getPVCoordinates(), 1.0));
194     }
195 
196     @Test
197     public void testIssue1118() throws URISyntaxException {
198         // Setup
199         final String fileName = "Jan2000F10-edited-data.txt";
200         final URL url = MarshallSolarActivityFutureEstimationTest.class.getClassLoader().getResource("atmosphere/" + fileName);
201         final DataSource source = new DataSource(url.toURI());
202         final MarshallSolarActivityFutureEstimation msafe =
203                 new MarshallSolarActivityFutureEstimation(source, StrengthLevel.AVERAGE);
204 
205         // Prepare data
206         final AbsoluteDate july = new AbsoluteDate(2008, 7, 1, utc);
207         final AbsoluteDate august = new AbsoluteDate(2008, 8, 1, utc);
208         final AbsoluteDate middle = july.shiftedBy(august.durationFrom(july) / 2.0);
209         final double minute = 60;
210         final AbsoluteDate before = middle.shiftedBy(-minute);
211         final AbsoluteDate after = middle.shiftedBy(+minute);
212 
213         // action + verify
214         // non-chaotic i.e. small change in input produces small change in output.
215         double kpHourlyDifference =
216                 msafe.getThreeHourlyKP(before) - msafe.getThreeHourlyKP(after);
217         assertThat(kpHourlyDifference, closeTo(0.0, 1e-4));
218         double kpDailyDifference = msafe.get24HoursKp(before) - msafe.get24HoursKp(after);
219         assertThat(kpDailyDifference, closeTo(0.0, 1e-4));
220         assertThat(msafe.getThreeHourlyKP(middle), closeTo(2.18, 0.3));
221         assertThat(msafe.get24HoursKp(middle), closeTo(2.18, 0.3));
222     }
223 
224     /**
225      * Configure a numerical propagator with DTM2000 atmosphere.
226      *
227      * @param sun   Sun.
228      * @param earth Earth.
229      * @param ic    initial condition.
230      * @return a propagator with DTM2000 atmosphere.
231      */
232     private NumericalPropagator getNumericalPropagatorWithDTM(CelestialBody sun,
233                                                               OneAxisEllipsoid earth,
234                                                               SpacecraftState ic)
235     {
236         // some non-integer step size to induce truncation error in flux interpolation
237         final ODEIntegrator integrator = new ClassicalRungeKuttaIntegrator(120 + 0.1);
238         NumericalPropagator propagator = new NumericalPropagator(integrator);
239         DTM2000InputParameters flux = getFlux();
240         final Atmosphere atmosphere = new DTM2000(flux, sun, earth);
241         final IsotropicDrag satellite = new IsotropicDrag(1, 3.2);
242         propagator.addForceModel(new DragForce(atmosphere, satellite));
243 
244         propagator.setInitialState(ic);
245         propagator.setOrbitType(OrbitType.CARTESIAN);
246 
247         return propagator;
248     }
249 
250     /**
251      * Configure a numerical propagator with NRLMSISE00 atmosphere.
252      *
253      * @param sun   Sun.
254      * @param earth Earth.
255      * @param ic    initial condition.
256      * @return a propagator with NRLMSISE00 atmosphere.
257      */
258     private NumericalPropagator getNumericalPropagatorWithMSIS(CelestialBody sun,
259                                                                OneAxisEllipsoid earth,
260                                                                SpacecraftState ic)
261     {
262         // some non-integer step size to induce truncation error in flux interpolation
263         final ODEIntegrator integrator = new ClassicalRungeKuttaIntegrator(120 + 0.1);
264         NumericalPropagator propagator = new NumericalPropagator(integrator);
265         NRLMSISE00InputParameters flux = getFlux();
266         final Atmosphere atmosphere = new NRLMSISE00(flux, sun, earth);
267         final IsotropicDrag satellite = new IsotropicDrag(1, 3.2);
268         propagator.addForceModel(new DragForce(atmosphere, satellite));
269 
270         propagator.setInitialState(ic);
271         propagator.setOrbitType(OrbitType.CARTESIAN);
272 
273         return propagator;
274     }
275 
276     /**
277      * Load an edited flux file.
278      *
279      * @return loaded flux file.
280      */
281     private MarshallSolarActivityFutureEstimation getFlux() {
282         final String fileName = "Jan2000F10-edited-data.txt";
283         try {
284             final URL url =
285                     MarshallSolarActivityFutureEstimationTest.class.getClassLoader().getResource("atmosphere/" + fileName);
286 
287             final DataSource source = new DataSource(url.toURI());
288             return new MarshallSolarActivityFutureEstimation(source, StrengthLevel.AVERAGE);
289         }
290         catch (URISyntaxException e) {
291             return null;
292         }
293     }
294 
295     @Test
296     public void testFileDate() {
297 
298         MarshallSolarActivityFutureEstimation msafe =
299             loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.AVERAGE);
300         Assertions.assertEquals(new DateComponents(2010, Month.NOVEMBER, 1),
301                             msafe.getFileDate(new AbsoluteDate("2010-05-01", utc)));
302         Assertions.assertEquals(new DateComponents(2010, Month.DECEMBER, 1),
303                             msafe.getFileDate(new AbsoluteDate("2010-06-01", utc)));
304         Assertions.assertEquals(new DateComponents(2011, Month.JANUARY, 1),
305                             msafe.getFileDate(new AbsoluteDate("2010-07-01", utc)));
306         Assertions.assertEquals(new DateComponents(2011, Month.JANUARY, 1),
307                             msafe.getFileDate(new AbsoluteDate("2030-01-01", utc)));
308 
309     }
310 
311     @Test
312     public void testFluxStrong() {
313 
314         MarshallSolarActivityFutureEstimation msafe =
315             loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.STRONG);
316         Assertions.assertEquals(94.2,
317                             msafe.getMeanFlux(new AbsoluteDate("2010-10-01", utc)),
318                             1.0e-10);
319         Assertions.assertEquals(msafe.getMeanFlux(new AbsoluteDate("2010-10-01", utc)),
320                             msafe.getDailyFlux(new AbsoluteDate("2010-10-02", utc)),
321                             1.0e-10);
322         Assertions.assertEquals(96.6,
323                             msafe.getMeanFlux(new AbsoluteDate("2010-10-16T12:00:00", utc)),
324                             1.0e-10);
325         Assertions.assertEquals(msafe.getMeanFlux(new AbsoluteDate("2010-10-16T12:00:00", utc)),
326                             msafe.getDailyFlux(new AbsoluteDate("2010-10-17T12:00:00", utc)),
327                             1.0e-10);
328         Assertions.assertEquals(99.0,
329                             msafe.getMeanFlux(new AbsoluteDate("2010-11-01", utc)),
330                             1.0e-10);
331         Assertions.assertEquals(msafe.getInstantFlux(new AbsoluteDate("2010-11-01", utc)),
332                             msafe.getMeanFlux(new AbsoluteDate("2010-11-01", utc)),
333                             1.0e-10);
334         Assertions.assertEquals(msafe.getMeanFlux(new AbsoluteDate("2010-11-01", utc)),
335                             msafe.getDailyFlux(new AbsoluteDate("2010-11-02", utc)),
336                             1.0e-10);
337         Assertions.assertEquals(msafe.getInstantFlux(new AbsoluteDate("2010-11-01", utc)),
338                             msafe.getDailyFlux(new AbsoluteDate("2010-11-02", utc)),
339                             1.0e-10);
340         Assertions.assertEquals(MarshallSolarActivityFutureEstimation.StrengthLevel.STRONG,
341                             msafe.getStrengthLevel());
342     }
343 
344 
345     @Test
346     public void testFluxAverage() {
347 
348         MarshallSolarActivityFutureEstimation msafe =
349             loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.AVERAGE);
350         Assertions.assertEquals(87.6,
351                             msafe.getMeanFlux(new AbsoluteDate("2010-10-01", utc)),
352                             1.0e-10);
353         Assertions.assertEquals(msafe.getMeanFlux(new AbsoluteDate("2010-10-01", utc)),
354                             msafe.getDailyFlux(new AbsoluteDate("2010-10-02", utc)),
355                             1.0e-10);
356         Assertions.assertEquals(88.7,
357                             msafe.getMeanFlux(new AbsoluteDate("2010-10-16T12:00:00", utc)),
358                             1.0e-10);
359         Assertions.assertEquals(msafe.getMeanFlux(new AbsoluteDate("2010-10-16T12:00:00", utc)),
360                             msafe.getDailyFlux(new AbsoluteDate("2010-10-17T12:00:00", utc)),
361                             1.0e-10);
362         Assertions.assertEquals(89.8,
363                             msafe.getMeanFlux(new AbsoluteDate("2010-11-01", utc)),
364                             1.0e-10);
365         Assertions.assertEquals(msafe.getInstantFlux(new AbsoluteDate("2010-11-01", utc)),
366                             msafe.getMeanFlux(new AbsoluteDate("2010-11-01", utc)),
367                             1.0e-10);
368         Assertions.assertEquals(msafe.getMeanFlux(new AbsoluteDate("2010-11-01", utc)),
369                             msafe.getDailyFlux(new AbsoluteDate("2010-11-02", utc)),
370                             1.0e-10);
371         Assertions.assertEquals(msafe.getInstantFlux(new AbsoluteDate("2010-11-01", utc)),
372                             msafe.getDailyFlux(new AbsoluteDate("2010-11-02", utc)),
373                             1.0e-10);
374         Assertions.assertEquals(MarshallSolarActivityFutureEstimation.StrengthLevel.AVERAGE,
375                             msafe.getStrengthLevel());
376     }
377 
378 
379     @Test
380     public void testFluxWeak() {
381 
382         MarshallSolarActivityFutureEstimation msafe =
383                 loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.WEAK);
384         Assertions.assertEquals(80.4,
385                             msafe.getMeanFlux(new AbsoluteDate("2010-10-01", utc)),
386                             1.0e-10);
387         Assertions.assertEquals(msafe.getMeanFlux(new AbsoluteDate("2010-10-01", utc)),
388                             msafe.getDailyFlux(new AbsoluteDate("2010-10-02", utc)),
389                             1.0e-10);
390         Assertions.assertEquals(80.6,
391                             msafe.getMeanFlux(new AbsoluteDate("2010-10-16T12:00:00", utc)),
392                             1.0e-10);
393         Assertions.assertEquals(msafe.getMeanFlux(new AbsoluteDate("2010-10-16T12:00:00", utc)),
394                             msafe.getDailyFlux(new AbsoluteDate("2010-10-17T12:00:00", utc)),
395                             1.0e-10);
396         Assertions.assertEquals(80.8,
397                             msafe.getMeanFlux(new AbsoluteDate("2010-11-01", utc)),
398                             1.0e-10);
399         Assertions.assertEquals(msafe.getInstantFlux(new AbsoluteDate("2010-11-01", utc)),
400                             msafe.getMeanFlux(new AbsoluteDate("2010-11-01", utc)),
401                             1.0e-10);
402         Assertions.assertEquals(msafe.getMeanFlux(new AbsoluteDate("2010-11-01", utc)),
403                             msafe.getDailyFlux(new AbsoluteDate("2010-11-02", utc)),
404                             1.0e-10);
405         Assertions.assertEquals(msafe.getInstantFlux(new AbsoluteDate("2010-11-01", utc)),
406                             msafe.getDailyFlux(new AbsoluteDate("2010-11-02", utc)),
407                             1.0e-10);
408         Assertions.assertEquals(MarshallSolarActivityFutureEstimation.StrengthLevel.WEAK,
409                             msafe.getStrengthLevel());
410 
411     }
412 
413     private MarshallSolarActivityFutureEstimation loadDefaultMSAFE(
414             MarshallSolarActivityFutureEstimation.StrengthLevel strength) {
415         return new MarshallSolarActivityFutureEstimation(MarshallSolarActivityFutureEstimation.DEFAULT_SUPPORTED_NAMES,
416                                                          strength);
417     }
418 
419     @Test
420     public void testKpStrong() {
421 
422         MarshallSolarActivityFutureEstimation msafe =
423             loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.STRONG);
424         Assertions.assertEquals(2 + 1.0 / 3.0,
425                             msafe.get24HoursKp(new AbsoluteDate("2010-10-01", utc)),
426                             0.2);
427         Assertions.assertEquals(3.0,
428                             msafe.get24HoursKp(new AbsoluteDate("2011-05-01", utc)),
429                             0.1);
430 
431         // this one should get exactly to an element of the AP_ARRAY: ap = 7.0
432         Assertions.assertEquals(2.0,
433                             msafe.get24HoursKp(new AbsoluteDate("2010-08-01", utc)),
434                             0.3);
435         Assertions.assertEquals(msafe.getThreeHourlyKP(new AbsoluteDate("2010-08-01", utc)),
436                             msafe.get24HoursKp(new AbsoluteDate("2010-08-01", utc)),
437                             1.0e-14);
438 
439     }
440 
441     @Test
442     public void testKpAverage() {
443 
444         MarshallSolarActivityFutureEstimation msafe =
445             loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.AVERAGE);
446         Assertions.assertEquals(2 - 1.0 / 3.0,
447                             msafe.get24HoursKp(new AbsoluteDate("2010-10-01", utc)),
448                             0.1);
449         Assertions.assertEquals(2 + 1.0 / 3.0,
450                             msafe.get24HoursKp(new AbsoluteDate("2011-05-01", utc)),
451                             0.1);
452         Assertions.assertEquals(2.0 - 1.0 / 3.0,
453                             msafe.get24HoursKp(new AbsoluteDate("2010-08-01", utc)),
454                             0.1);
455         Assertions.assertEquals(msafe.getThreeHourlyKP(new AbsoluteDate("2010-08-01", utc)),
456                             msafe.get24HoursKp(new AbsoluteDate("2010-08-01", utc)),
457                             1.0e-14);
458 
459     }
460 
461     @Test
462     public void testKpWeak() {
463 
464         MarshallSolarActivityFutureEstimation msafe =
465             loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.WEAK);
466         Assertions.assertEquals(1 + 1.0 / 3.0,
467                             msafe.get24HoursKp(new AbsoluteDate("2010-10-01", utc)),
468                             0.1);
469         Assertions.assertEquals(2.0,
470                             msafe.get24HoursKp(new AbsoluteDate("2011-05-01", utc)),
471                             0.3);
472         Assertions.assertEquals(1 + 1.0 / 3.0,
473                             msafe.get24HoursKp(new AbsoluteDate("2010-08-01", utc)),
474                             0.1);
475         Assertions.assertEquals(msafe.getThreeHourlyKP(new AbsoluteDate("2010-08-01", utc)),
476                             msafe.get24HoursKp(new AbsoluteDate("2010-08-01", utc)),
477                             1.0e-14);
478 
479     }
480 
481     @Test
482     public void testApStrong() {
483 
484         MarshallSolarActivityFutureEstimation msafe =
485             loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.STRONG);
486         for (double ap: msafe.getAp(new AbsoluteDate("2010-10-01", utc))) {
487             Assertions.assertEquals(9.1, ap, 1e-10);
488         }
489         for (double ap: msafe.getAp(new AbsoluteDate("2011-05-01", utc))) {
490             Assertions.assertEquals(14.4, ap, 1e-10);
491         }
492         for (double ap: msafe.getAp(new AbsoluteDate("2010-08-01", utc))) {
493             Assertions.assertEquals(7.0, ap, 1e-10);
494         }
495     }
496 
497     @Test
498     public void testApAverage() {
499 
500         MarshallSolarActivityFutureEstimation msafe =
501             loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.AVERAGE);
502         for (double ap: msafe.getAp(new AbsoluteDate("2010-10-01", utc))) {
503             Assertions.assertEquals(6.4, ap, 1e-10);
504         }
505         for (double ap: msafe.getAp(new AbsoluteDate("2011-05-01", utc))) {
506             Assertions.assertEquals(9.6, ap, 1e-10);
507         }
508         for (double ap: msafe.getAp(new AbsoluteDate("2010-08-01", utc))) {
509             Assertions.assertEquals(6.1, ap, 1e-10);
510         }
511     }
512 
513     @Test
514     public void testApWeak() {
515 
516         MarshallSolarActivityFutureEstimation msafe =
517             loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.WEAK);
518         for (double ap: msafe.getAp(new AbsoluteDate("2010-10-01", utc))) {
519             Assertions.assertEquals(4.9, ap, 1e-10);
520         }
521         for (double ap: msafe.getAp(new AbsoluteDate("2011-05-01", utc))) {
522             Assertions.assertEquals(6.9, ap, 1e-10);
523         }
524         for (double ap: msafe.getAp(new AbsoluteDate("2010-08-01", utc))) {
525             Assertions.assertEquals(4.9, ap, 1e-10);
526         }
527     }
528 
529     @Test
530     public void testMinDate() {
531 
532         MarshallSolarActivityFutureEstimation msafe =
533             new MarshallSolarActivityFutureEstimation(MarshallSolarActivityFutureEstimation.DEFAULT_SUPPORTED_NAMES,
534                                                       MarshallSolarActivityFutureEstimation.StrengthLevel.WEAK);
535         Assertions.assertEquals(new AbsoluteDate("2010-05-01", utc), msafe.getMinDate());
536         Assertions.assertEquals(78.1,
537                             msafe.getMeanFlux(msafe.getMinDate()),
538                             1.0e-14);
539     }
540 
541     @Test
542     public void testMaxDate() {
543 
544         MarshallSolarActivityFutureEstimation msafe =
545             new MarshallSolarActivityFutureEstimation(MarshallSolarActivityFutureEstimation.DEFAULT_SUPPORTED_NAMES,
546                                                       MarshallSolarActivityFutureEstimation.StrengthLevel.WEAK);
547         Assertions.assertEquals(new AbsoluteDate("2030-10-01", utc), msafe.getMaxDate());
548         Assertions.assertEquals(67.0, msafe.getMeanFlux(msafe.getMaxDate()), 1.0e-14);
549     }
550 
551     @Test
552     public void testPastOutOfRange() {
553         Assertions.assertThrows(OrekitException.class, () -> {
554             MarshallSolarActivityFutureEstimation msafe =
555                     loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.WEAK);
556             msafe.get24HoursKp(new AbsoluteDate("1960-10-01", utc));
557         });
558     }
559 
560     @Test
561     public void testFutureOutOfRange() {
562         Assertions.assertThrows(OrekitException.class, () -> {
563             MarshallSolarActivityFutureEstimation msafe =
564                     loadDefaultMSAFE(MarshallSolarActivityFutureEstimation.StrengthLevel.WEAK);
565             msafe.get24HoursKp(new AbsoluteDate("2060-10-01", utc));
566         });
567     }
568 
569     @Test
570     public void testExtraData() {
571         Assertions.assertThrows(OrekitException.class, () -> {
572                     new MarshallSolarActivityFutureEstimation("Jan2011F10-extra-data\\.txt",
573                             MarshallSolarActivityFutureEstimation.StrengthLevel.STRONG);
574         });
575     }
576 
577     @Test
578     public void testNoData() {
579         Assertions.assertThrows(OrekitException.class, () -> {
580             new MarshallSolarActivityFutureEstimation("Jan2011F10-no-data\\.txt",
581                                                       MarshallSolarActivityFutureEstimation.StrengthLevel.STRONG);
582         });
583     }
584 
585     /**
586      * This test in a multi-threaded environment would not necessarily fail without the fix (even though it will very likely
587      * fail).
588      * <p>
589      * However, it cannot fail with the fix.
590      */
591     @RepeatedTest(10)
592     @DisplayName("Test in a multi-threaded environment")
593     void testIssue1072() {
594         // GIVEN
595         final MarshallSolarActivityFutureEstimation weatherData = loadDefaultMSAFE(StrengthLevel.AVERAGE);
596 
597         // Create date sample at which flux will be evaluated
598         final AbsoluteDate       initialDate = new AbsoluteDate("2010-05-01", utc);
599         final List<AbsoluteDate> dates       = new ArrayList<>();
600         final int                sampleSize  = 100;
601         for (int i = 0; i < sampleSize + 1; i++) {
602             dates.add(initialDate.shiftedBy(i * Constants.JULIAN_DAY * 30));
603         }
604 
605         // Create list of tasks to run in parallel
606         final AtomicReference<List<TimeStampedDouble>> results = new AtomicReference<>(new ArrayList<>());
607         final List<Callable<List<TimeStampedDouble>>>  tasks   = new ArrayList<>();
608         for (int i = 0; i < sampleSize + 1; i++) {
609             final AbsoluteDate currentDate = dates.get(i);
610             // Each task will evaluate value at specific date and store this value and associated date in a shared list
611             tasks.add(() -> (results.getAndUpdate((listToUpdate) -> {
612                 final List<TimeStampedDouble> newList = new ArrayList<>(listToUpdate);
613                 newList.add(new TimeStampedDouble(weatherData.get24HoursKp(currentDate), currentDate));
614                 return newList;
615             })));
616         }
617 
618         // Create multithreading environment
619         ExecutorService service = Executors.newFixedThreadPool(sampleSize);
620 
621         // WHEN
622         try {
623             service.invokeAll(tasks);
624             results.get().sort(Comparator.comparing(TimeStampedDouble::getDate));
625             final List<Double> sortedComputedResults = results.get().stream().map(TimeStampedDouble::getValue).collect(
626                     Collectors.toList());
627 
628             // THEN
629             // Compare to expected result
630             for (int i = 0; i < sampleSize + 1; i++) {
631                 final AbsoluteDate currentDate = dates.get(i);
632                 Assertions.assertEquals(weatherData.get24HoursKp(currentDate), sortedComputedResults.get(i));
633             }
634 
635             try {
636                 // wait for proper ending
637                 service.shutdown();
638                 service.awaitTermination(5, TimeUnit.SECONDS);
639             } catch (InterruptedException ie) {
640                 // Restore interrupted state...
641                 Thread.currentThread().interrupt();
642             }
643         }
644         catch (Exception e) {
645             // Should not fail
646             Assertions.fail();
647         }
648     }
649 
650     @Test
651     void testExpectedCacheConfigurationAndCalls() {
652         // GIVEN
653         final AbsoluteDate date = new AbsoluteDate(2020, 2, 25, 2, 0, 0, TimeScalesFactory.getUTC());
654 
655         final MarshallSolarActivityFutureEstimation atm =
656               new MarshallSolarActivityFutureEstimation(MarshallSolarActivityFutureEstimation.DEFAULT_SUPPORTED_NAMES,
657                                                         StrengthLevel.AVERAGE);
658 
659         // WHEN
660         // Call flux at instants that shall generate slots
661         atm.getInstantFlux(date.shiftedBy(-1 * Constants.JULIAN_DAY * 31));
662         atm.getInstantFlux(date);
663         atm.getInstantFlux(date.shiftedBy(1 * Constants.JULIAN_DAY * 31));
664         atm.getInstantFlux(date.shiftedBy(3 * Constants.JULIAN_DAY * 31));
665         atm.getInstantFlux(date.shiftedBy(2 * Constants.JULIAN_DAY * 31));
666 
667         // Call flux at instants that shall not generate slots
668         atm.getInstantFlux(date.shiftedBy(-0.6 * Constants.JULIAN_DAY * 31));
669         atm.getInstantFlux(date.shiftedBy(1.8 * Constants.JULIAN_DAY * 31));
670 
671         // THEN
672         final GenericTimeStampedCache<MarshallSolarActivityFutureEstimationLoader.LineParameters> cache = atm.getCache();
673         Assertions.assertEquals(5, cache.getGenerateCalls());
674         Assertions.assertEquals(5, cache.getSlots());
675         Assertions.assertEquals(10, cache.getEntries());
676         Assertions.assertEquals(7, cache.getGetNeighborsCalls());
677     }
678 
679     @BeforeEach
680     public void setUp() {
681         Utils.setDataRoot("regular-data:atmosphere");
682         utc = TimeScalesFactory.getUTC();
683     }
684 
685     @AfterEach
686     public void tearDown() {
687         utc = null;
688     }
689 
690     private TimeScale utc;
691 
692 }