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.utils;
18  
19  import org.hamcrest.CoreMatchers;
20  import org.hamcrest.MatcherAssert;
21  import org.hipparchus.random.RandomGenerator;
22  import org.hipparchus.random.Well1024a;
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.Test;
27  import org.orekit.Utils;
28  import org.orekit.bodies.GeodeticPoint;
29  import org.orekit.errors.OrekitIllegalArgumentException;
30  import org.orekit.errors.TimeStampedCacheException;
31  import org.orekit.frames.Frame;
32  import org.orekit.frames.FramesFactory;
33  import org.orekit.frames.TopocentricFrame;
34  import org.orekit.models.earth.ReferenceEllipsoid;
35  import org.orekit.propagation.analytical.tle.TLE;
36  import org.orekit.propagation.analytical.tle.TLEPropagator;
37  import org.orekit.propagation.events.ElevationDetector;
38  import org.orekit.time.AbsoluteDate;
39  import org.orekit.time.TimeScale;
40  import org.orekit.time.TimeScalesFactory;
41  
42  import java.util.ArrayList;
43  import java.util.Collections;
44  import java.util.List;
45  import java.util.concurrent.ExecutorService;
46  import java.util.concurrent.Executors;
47  import java.util.concurrent.TimeUnit;
48  import java.util.concurrent.atomic.AtomicReference;
49  import java.util.stream.Collectors;
50  
51  
52  public class GenericTimeStampedCacheTest {
53  
54      @Test
55      public void testSingleCall() throws TimeStampedCacheException {
56          GenericTimeStampedCache<AbsoluteDate> cache = createCache(10, 3600.0, 13);
57          List<AbsoluteDate> list = new ArrayList<AbsoluteDate>();
58          list.add(AbsoluteDate.GALILEO_EPOCH);
59          Assertions.assertEquals(1, checkDatesSingleThread(list, cache));
60          Assertions.assertEquals(1, cache.getGetNeighborsCalls());
61          Assertions.assertEquals(4, cache.getGenerateCalls());
62          Assertions.assertEquals(0, cache.getSlotsEvictions());
63          Assertions.assertEquals(10, cache.getMaxSlots());
64          Assertions.assertEquals(Constants.JULIAN_DAY, cache.getNewSlotQuantumGap(), 1.0e-10);
65          Assertions.assertEquals(Constants.JULIAN_YEAR, cache.getMaxSpan(), 1.0e-10);
66      }
67  
68      @Test
69      public void testPastInfinityRange() throws TimeStampedCacheException {
70          GenericTimeStampedCache<AbsoluteDate> cache =
71                  new GenericTimeStampedCache<AbsoluteDate>(2, 10, Constants.JULIAN_YEAR, Constants.JULIAN_DAY,
72                                                     new Generator(AbsoluteDate.PAST_INFINITY,
73                                                                   AbsoluteDate.J2000_EPOCH,
74                                                                   10.0));
75          List<AbsoluteDate> list = new ArrayList<AbsoluteDate>();
76          list.add(AbsoluteDate.GALILEO_EPOCH);
77          list.add(AbsoluteDate.MODIFIED_JULIAN_EPOCH);
78          list.add(AbsoluteDate.JULIAN_EPOCH);
79          Assertions.assertEquals(3, checkDatesSingleThread(list, cache));
80          Assertions.assertEquals(3, cache.getGetNeighborsCalls());
81          try {
82              cache.getNeighbors(AbsoluteDate.J2000_EPOCH.shiftedBy(100.0));
83              Assertions.fail("expected TimeStampedCacheException");
84          } catch (TimeStampedCacheException tce) {
85              // expected behavior
86          } catch (Exception e) {
87              Assertions.fail("wrong exception caught");
88          }
89      }
90  
91      @Test
92      public void testFutureInfinityRange() throws TimeStampedCacheException {
93          GenericTimeStampedCache<AbsoluteDate> cache =
94                  new GenericTimeStampedCache<AbsoluteDate>(2, 10, Constants.JULIAN_YEAR, Constants.JULIAN_DAY,
95                                                     new Generator(AbsoluteDate.MODIFIED_JULIAN_EPOCH,
96                                                                   AbsoluteDate.FUTURE_INFINITY, 10.0));
97          List<AbsoluteDate> list = new ArrayList<AbsoluteDate>();
98          list.add(AbsoluteDate.J2000_EPOCH);
99          list.add(AbsoluteDate.GALILEO_EPOCH);
100         Assertions.assertEquals(2, checkDatesSingleThread(list, cache));
101         Assertions.assertEquals(2, cache.getGetNeighborsCalls());
102         try {
103             cache.getNeighbors(AbsoluteDate.JULIAN_EPOCH);
104             Assertions.fail("expected TimeStampedCacheException");
105         } catch (TimeStampedCacheException tce) {
106             // expected behavior
107         } catch (Exception e) {
108             Assertions.fail("wrong exception caught");
109         }
110     }
111 
112     @Test
113     public void testInfinityRange() throws TimeStampedCacheException {
114         GenericTimeStampedCache<AbsoluteDate> cache =
115                 new GenericTimeStampedCache<AbsoluteDate>(2, 10, Constants.JULIAN_YEAR, Constants.JULIAN_DAY,
116                                                    new Generator(AbsoluteDate.PAST_INFINITY,
117                                                                  AbsoluteDate.FUTURE_INFINITY,
118                                                                  10.0));
119         List<AbsoluteDate> list = new ArrayList<AbsoluteDate>();
120         list.add(AbsoluteDate.J2000_EPOCH.shiftedBy(+4.6e12));
121         list.add(AbsoluteDate.J2000_EPOCH.shiftedBy(-4.6e12));
122         list.add(AbsoluteDate.JULIAN_EPOCH);
123         list.add(AbsoluteDate.J2000_EPOCH);
124         list.add(AbsoluteDate.GALILEO_EPOCH);
125         Assertions.assertEquals(5, checkDatesSingleThread(list, cache));
126         Assertions.assertEquals(5, cache.getGetNeighborsCalls());
127     }
128 
129     @Test
130     public void testRegularCalls() throws TimeStampedCacheException {
131         GenericTimeStampedCache<AbsoluteDate> cache = createCache(2, 3600, 13);
132         Assertions.assertEquals(2000, testMultipleSingleThread(cache, new SequentialMode(), 2));
133         Assertions.assertEquals(2000, cache.getGetNeighborsCalls());
134         Assertions.assertEquals(56, cache.getGenerateCalls());
135         Assertions.assertEquals(0, cache.getSlotsEvictions());
136     }
137 
138     @Test
139     public void testAlternateCallsGoodConfiguration() throws TimeStampedCacheException {
140         GenericTimeStampedCache<AbsoluteDate> cache = createCache(2, 3600, 13);
141         Assertions.assertEquals(2000, testMultipleSingleThread(cache, new AlternateMode(), 2));
142         Assertions.assertEquals(2000, cache.getGetNeighborsCalls());
143         Assertions.assertEquals(56, cache.getGenerateCalls());
144         Assertions.assertEquals(0, cache.getSlotsEvictions());
145     }
146 
147     @Test
148     public void testAlternateCallsBadConfiguration() throws TimeStampedCacheException {
149         GenericTimeStampedCache<AbsoluteDate> cache = createCache(1, 3600, 13);
150         Assertions.assertEquals(2000, testMultipleSingleThread(cache, new AlternateMode(), 2));
151         Assertions.assertEquals(2000, cache.getGetNeighborsCalls());
152         Assertions.assertEquals(8000, cache.getGenerateCalls());
153         Assertions.assertEquals(1999, cache.getSlotsEvictions());
154     }
155 
156     @Test
157     public void testRandomCallsGoodConfiguration() throws TimeStampedCacheException {
158         GenericTimeStampedCache<AbsoluteDate> cache = createCache(30, 3600, 13);
159         Assertions.assertEquals(5000, testMultipleSingleThread(cache, new RandomMode(64394632125212l), 5));
160         Assertions.assertEquals(5000, cache.getGetNeighborsCalls());
161         Assertions.assertTrue(cache.getGenerateCalls() < 250);
162         Assertions.assertEquals(0, cache.getSlotsEvictions());
163     }
164 
165     @Test
166     public void testRandomCallsBadConfiguration() throws TimeStampedCacheException {
167         GenericTimeStampedCache<AbsoluteDate> cache = createCache(3, 3600, 13);
168         Assertions.assertEquals(5000, testMultipleSingleThread(cache, new RandomMode(64394632125212l), 5));
169         Assertions.assertEquals(5000, cache.getGetNeighborsCalls());
170         Assertions.assertTrue(cache.getGenerateCalls()  > 400);
171         Assertions.assertTrue(cache.getSlotsEvictions() > 300);
172     }
173 
174     @Test
175     public void testMultithreadedGoodConfiguration() throws TimeStampedCacheException {
176         GenericTimeStampedCache<AbsoluteDate> cache = createCache(50, 3600, 13);
177         int n = testMultipleMultiThread(cache, new AlternateMode(), 50, 30);
178         Assertions.assertEquals(n, cache.getGetNeighborsCalls());
179         Assertions.assertTrue(cache.getGenerateCalls() < n / 20,
180                 "this test may fail randomly due to multi-threading non-determinism" +
181                 " (n = " + n + ", calls = " + cache.getGenerateCalls() +
182                 ", ratio = " + (n / cache.getGenerateCalls()) + ")");
183         Assertions.assertTrue(cache.getSlotsEvictions() < n / 1000, 
184                 "this test may fail randomly due to multi-threading non-determinism" +
185                 " (n = " + n + ", evictions = " + cache.getSlotsEvictions() +
186                 (cache.getSlotsEvictions() == 0 ? "" : (", ratio = " + (n / cache.getSlotsEvictions()))) + ")");
187     }
188 
189     @Test
190     public void testMultithreadedBadConfiguration() throws TimeStampedCacheException {
191         GenericTimeStampedCache<AbsoluteDate> cache = createCache(3, 3600, 13);
192         int n = testMultipleMultiThread(cache, new AlternateMode(), 50, 100);
193         Assertions.assertEquals(n, cache.getGetNeighborsCalls());
194         Assertions.assertTrue(cache.getGenerateCalls() > n / 15,
195                 "this test may fail randomly due to multi-threading non-determinism" +
196                 " (n = " + n + ", calls = " + cache.getGenerateCalls() +
197                 ", ratio = " + (n / cache.getGenerateCalls()) + ")");
198         Assertions.assertTrue(cache.getSlotsEvictions() > n / 60, 
199                 "this test may fail randomly due to multi-threading non-determinism" +
200                 " (n = " + n + ", evictions = " + cache.getSlotsEvictions() +
201                 ", ratio = " + (n / cache.getSlotsEvictions()) + ")");
202     }
203 
204     @Test
205     public void testSmallShift() throws TimeStampedCacheException {
206         double hour = 3600;
207         GenericTimeStampedCache<AbsoluteDate> cache = createCache(10, hour, 13);
208         Assertions.assertEquals(0, cache.getSlots());
209         Assertions.assertEquals(0, cache.getEntries());
210         final AbsoluteDate start = AbsoluteDate.GALILEO_EPOCH;
211         cache.getNeighbors(start);
212         Assertions.assertEquals(1, cache.getGetNeighborsCalls());
213         Assertions.assertEquals(1, cache.getSlots());
214         Assertions.assertEquals(18, cache.getEntries());
215         Assertions.assertEquals(4, cache.getGenerateCalls());
216         Assertions.assertEquals(-11 * hour, cache.getEarliest().durationFrom(start), 1.0e-10);
217         Assertions.assertEquals( +6 * hour, cache.getLatest().durationFrom(start), 1.0e-10);
218         cache.getNeighbors(start.shiftedBy(-3 * 3600));
219         Assertions.assertEquals(2, cache.getGetNeighborsCalls());
220         Assertions.assertEquals(1, cache.getSlots());
221         Assertions.assertEquals(18, cache.getEntries());
222         Assertions.assertEquals(4, cache.getGenerateCalls());
223         Assertions.assertEquals(-11 * hour, cache.getEarliest().durationFrom(start), 1.0e-10);
224         Assertions.assertEquals( +6 * hour, cache.getLatest().durationFrom(start), 1.0e-10);
225         cache.getNeighbors(start.shiftedBy(7 * 3600));
226         Assertions.assertEquals(3, cache.getGetNeighborsCalls());
227         Assertions.assertEquals(1, cache.getSlots());
228         Assertions.assertEquals(25, cache.getEntries());
229         Assertions.assertEquals(5, cache.getGenerateCalls());
230         Assertions.assertEquals(-11 * hour, cache.getEarliest().durationFrom(start), 1.0e-10);
231         Assertions.assertEquals(+13 * hour, cache.getLatest().durationFrom(start), 1.0e-10);
232     }
233 
234     @Test
235     public void testNotEnoughSlots() {
236         Assertions.assertThrows(IllegalArgumentException.class, () -> {
237             createCache(0, 3600.0, 13);
238         });
239     }
240 
241     @Test
242     public void testNotEnoughNeighbors() {
243         Assertions.assertThrows(IllegalArgumentException.class, () -> {
244             createCache(10, 3600.0, 1);
245         });
246     }
247 
248     @Test
249     public void testNoEarliestEntry() {
250         Assertions.assertThrows(IllegalStateException.class, () -> {
251             createCache(10, 3600.0, 3).getEarliest();
252         });
253     }
254 
255     @Test
256     public void testNoLatestEntry() {
257         Assertions.assertThrows(IllegalStateException.class, () -> {
258             createCache(10, 3600.0, 3).getLatest();
259         });
260     }
261 
262     @Test
263     public void testNoGeneratedData() throws TimeStampedCacheException {
264         Assertions.assertThrows(TimeStampedCacheException.class, () -> {
265             TimeStampedGenerator<AbsoluteDate> nullGenerator =
266                     new TimeStampedGenerator<AbsoluteDate>() {
267                         public List<AbsoluteDate> generate(AbsoluteDate existingDate,
268                                 AbsoluteDate date) {
269                             return new ArrayList<AbsoluteDate>();
270                         }
271                     };
272             new GenericTimeStampedCache<AbsoluteDate>(2, 10, Constants.JULIAN_YEAR, Constants.JULIAN_DAY,
273                     nullGenerator).getNeighbors(AbsoluteDate.J2000_EPOCH);
274         });
275     }
276 
277     @Test
278     public void testNoDataBefore() throws TimeStampedCacheException {
279         TimeStampedGenerator<AbsoluteDate> nullGenerator =
280                 new TimeStampedGenerator<AbsoluteDate>() {
281                     public List<AbsoluteDate> generate(AbsoluteDate existingDate,
282                                                        AbsoluteDate date) {
283                         return Collections.singletonList(AbsoluteDate.J2000_EPOCH);
284                     }
285                 };
286         AbsoluteDate central = AbsoluteDate.J2000_EPOCH.shiftedBy(-10);
287         GenericTimeStampedCache<AbsoluteDate> cache = new GenericTimeStampedCache<>(
288                 2, 10, Constants.JULIAN_YEAR, Constants.JULIAN_DAY, nullGenerator);
289         try {
290             cache.getNeighbors(central);
291             Assertions.fail("Expected Exception");
292         } catch (TimeStampedCacheException e) {
293             MatcherAssert.assertThat(e.getMessage(),
294                     CoreMatchers.containsString(central.toString()));
295         }
296     }
297 
298     @Test
299     public void testNoDataAfter() throws TimeStampedCacheException {
300         TimeStampedGenerator<AbsoluteDate> nullGenerator =
301                 new TimeStampedGenerator<AbsoluteDate>() {
302             public List<AbsoluteDate> generate(AbsoluteDate existingDate,
303                                                AbsoluteDate date) {
304                 return Collections.singletonList(AbsoluteDate.J2000_EPOCH);
305             }
306         };
307         AbsoluteDate central = AbsoluteDate.J2000_EPOCH.shiftedBy(+10);
308         GenericTimeStampedCache<AbsoluteDate> cache = new GenericTimeStampedCache<>(
309                 2, 10, Constants.JULIAN_YEAR, Constants.JULIAN_DAY, nullGenerator);
310         try {
311             cache.getNeighbors(central);
312             Assertions.fail("Expected Exception");
313         } catch (TimeStampedCacheException e) {
314             MatcherAssert.assertThat(e.getMessage(),
315                     CoreMatchers.containsString(central.toString()));
316         }
317     }
318 
319     @Test
320     public void testUnsortedEntries() throws TimeStampedCacheException {
321         Assertions.assertThrows(TimeStampedCacheException.class, () -> {
322             TimeStampedGenerator<AbsoluteDate> reversedGenerator =
323                     new TimeStampedGenerator<AbsoluteDate>() {
324                         /** {@inheritDoc} */
325                         public List<AbsoluteDate> generate(AbsoluteDate existingDate, AbsoluteDate date) {
326                             List<AbsoluteDate> list = new ArrayList<AbsoluteDate>();
327                             list.add(date);
328                             list.add(date.shiftedBy(-10.0));
329                             return list;
330                         }
331                     };
332 
333             new GenericTimeStampedCache<AbsoluteDate>(3, 10, Constants.JULIAN_YEAR, Constants.JULIAN_DAY,
334                     reversedGenerator).getNeighbors(AbsoluteDate.J2000_EPOCH);
335         });
336     }
337 
338     @Test
339     public void testDuplicatingGenerator() throws TimeStampedCacheException {
340 
341         final double step = 3600.0;
342 
343         TimeStampedGenerator<AbsoluteDate> duplicatingGenerator =
344                 new TimeStampedGenerator<AbsoluteDate>() {
345 
346             /** {@inheritDoc} */
347             public List<AbsoluteDate> generate(AbsoluteDate existingDate, AbsoluteDate date) {
348                 List<AbsoluteDate> list = new ArrayList<AbsoluteDate>();
349                 if (existingDate == null) {
350                     list.add(date);
351                 } else {
352                     if (date.compareTo(existingDate) > 0) {
353                         AbsoluteDate t = existingDate.shiftedBy(-10 * step);
354                         do {
355                             t = t.shiftedBy(step);
356                             list.add(list.size(), t);
357                         } while (t.compareTo(date) <= 0);
358                     } else {
359                         AbsoluteDate t = existingDate.shiftedBy(10 * step);
360                         do {
361                             t = t.shiftedBy(-step);
362                             list.add(0, t);
363                         } while (t.compareTo(date) >= 0);
364                     }
365                 }
366                 return list;
367             }
368 
369         };
370 
371         final GenericTimeStampedCache<AbsoluteDate> cache =
372                 new GenericTimeStampedCache<AbsoluteDate>(5, 10, Constants.JULIAN_YEAR, Constants.JULIAN_DAY,
373                                                    duplicatingGenerator);
374 
375         final AbsoluteDate start = AbsoluteDate.GALILEO_EPOCH;
376         final List<AbsoluteDate> firstSet = cache.getNeighbors(start).collect(Collectors.toList());
377         Assertions.assertEquals(5, firstSet.size());
378         Assertions.assertEquals(4, cache.getGenerateCalls());
379         Assertions.assertEquals(8, cache.getEntries());
380         for (int i = 1; i < firstSet.size(); ++i) {
381             Assertions.assertEquals(step, firstSet.get(i).durationFrom(firstSet.get(i - 1)), 1.0e-10);
382         }
383 
384         final List<AbsoluteDate> secondSet = cache.getNeighbors(cache.getLatest().shiftedBy(10 * step)).collect(Collectors.toList());
385         Assertions.assertEquals(5, secondSet.size());
386         Assertions.assertEquals(7, cache.getGenerateCalls());
387         Assertions.assertEquals(20, cache.getEntries());
388         for (int i = 1; i < secondSet.size(); ++i) {
389             Assertions.assertEquals(step, firstSet.get(i).durationFrom(firstSet.get(i - 1)), 1.0e-10);
390         }
391 
392     }
393 
394     @Test
395     @DisplayName("Test that the cache of the detector does not get corrupted when a propagation to infinity has been run")
396     void testIssue1108() {
397         // GIVEN
398         final TLE aTle = new TLE("1 27424U 02022A   23173.43403823  .00001056  00000+0  23935-3 0  9994",
399                            "2 27424  98.2874 117.8299 0001810 103.6635   5.7337 14.58117998124128");
400 
401         final Frame            itrf      = FramesFactory.getITRF(IERSConventions.IERS_2010, true);
402         final GeodeticPoint    site      = new GeodeticPoint(0.0, 0.0, 0.0);
403         final TopocentricFrame siteFrame = new TopocentricFrame(ReferenceEllipsoid.getIers2010(itrf), site, "site");
404         final ElevationDetector siteVisDetector =
405               new ElevationDetector(60, 0.001, siteFrame).withConstantElevation(5.0);
406 
407         // Create TLE propagator
408         final TLEPropagator tlePropagator = TLEPropagator.selectExtrapolator(aTle);
409         tlePropagator.addEventDetector(siteVisDetector);
410 
411         try {
412             // WHEN
413             // Propagation from and to infinity throws an exception
414             tlePropagator.propagate(AbsoluteDate.PAST_INFINITY, AbsoluteDate.FUTURE_INFINITY);
415         } catch (OrekitIllegalArgumentException e) {
416             // THEN
417             TimeScale          utc                = TimeScalesFactory.getUTC();
418             final AbsoluteDate ephemerisStartDate = new AbsoluteDate(23, 6, 15, 0, 0, 0, utc);
419             final AbsoluteDate ephemerisEndDate   = new AbsoluteDate(23, 6, 15, 0, 0, 1, utc);
420 
421             // New propagation should not throw an ArithmeticException
422             Assertions.assertDoesNotThrow(() -> tlePropagator.propagate(ephemerisStartDate, ephemerisEndDate));
423         }
424     }
425 
426     private int testMultipleSingleThread(GenericTimeStampedCache<AbsoluteDate> cache, Mode mode, int slots)
427         throws TimeStampedCacheException {
428         double step = ((Generator) cache.getGenerator()).getStep();
429         AbsoluteDate[] base = new AbsoluteDate[slots];
430         base[0] = AbsoluteDate.GALILEO_EPOCH;
431         for (int i = 1; i < base.length; ++i) {
432             base[i] = base[i - 1].shiftedBy(10 * Constants.JULIAN_DAY);
433         }
434         return checkDatesSingleThread(mode.generateDates(base, 25 * step, 0.025 * step), cache);
435     }
436 
437     private int testMultipleMultiThread(GenericTimeStampedCache<AbsoluteDate> cache, Mode mode,
438                                         int slots, int threadPoolSize)
439         throws TimeStampedCacheException {
440         double step = ((Generator) cache.getGenerator()).getStep();
441         AbsoluteDate[] base = new AbsoluteDate[slots];
442         base[0] = AbsoluteDate.GALILEO_EPOCH;
443         for (int i = 1; i < base.length; ++i) {
444             base[i] = base[i - 1].shiftedBy(10 * Constants.JULIAN_DAY);
445         }
446         return checkDatesMultiThread(mode.generateDates(base, 25 * step, 0.025 * step), cache, threadPoolSize);
447     }
448 
449     private GenericTimeStampedCache<AbsoluteDate> createCache(int maxSlots, double step, int neighborsSize) {
450         Generator generator =
451                 new Generator(AbsoluteDate.J2000_EPOCH.shiftedBy(-Constants.JULIAN_CENTURY),
452                               AbsoluteDate.J2000_EPOCH.shiftedBy(+Constants.JULIAN_CENTURY),
453                               step);
454         return new GenericTimeStampedCache<AbsoluteDate>(neighborsSize, maxSlots, Constants.JULIAN_YEAR,
455                                                   Constants.JULIAN_DAY, generator);
456     }
457 
458     private int checkDatesSingleThread(final List<AbsoluteDate> centralDates,
459                                        final GenericTimeStampedCache<AbsoluteDate> cache)
460         throws TimeStampedCacheException {
461 
462         final int n = cache.getMaxNeighborsSize();
463         final double step = ((Generator) cache.getGenerator()).getStep();
464 
465         for (final AbsoluteDate central : centralDates) {
466             final List<AbsoluteDate> neighbors = cache.getNeighbors(central).collect(Collectors.toList());
467             Assertions.assertEquals(n, neighbors.size());
468             for (final AbsoluteDate date : neighbors) {
469                 Assertions.assertTrue(date.durationFrom(central) >= -(n + 1) * step);
470                 Assertions.assertTrue(date.durationFrom(central) <= n * step);
471             }
472         }
473 
474         return centralDates.size();
475 
476     }
477 
478     private int checkDatesMultiThread(final List<AbsoluteDate> centralDates,
479                                       final GenericTimeStampedCache<AbsoluteDate> cache,
480                                       final int threadPoolSize)
481         throws TimeStampedCacheException {
482 
483         final int n = cache.getMaxNeighborsSize();
484         final double step = ((Generator) cache.getGenerator()).getStep();
485         final AtomicReference<AbsoluteDate[]> failedDates = new AtomicReference<AbsoluteDate[]>();
486         final AtomicReference<TimeStampedCacheException> caught = new AtomicReference<TimeStampedCacheException>();
487         ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);
488 
489         for (final AbsoluteDate central : centralDates) {
490             executorService.execute(new Runnable() {
491                 public void run() {
492                     try {
493                         final List<AbsoluteDate> neighbors = cache.getNeighbors(central).collect(Collectors.toList());
494                         Assertions.assertEquals(n, neighbors.size());
495                         for (final AbsoluteDate date : neighbors) {
496                             if (date.durationFrom(central) < -(n + 1) * step ||
497                                 date.durationFrom(central) > n * step) {
498                                 AbsoluteDate[] dates = new AbsoluteDate[n + 1];
499                                 dates[0] = central;
500                                 System.arraycopy(neighbors, 0, dates, 1, n);
501                                 failedDates.set(dates);
502                             }
503                         }
504                     } catch (TimeStampedCacheException tce) {
505                         caught.set(tce);
506                     }
507                 }
508             });
509         }
510 
511         try {
512             executorService.shutdown();
513             Assertions.assertTrue(executorService.awaitTermination(10, TimeUnit.MINUTES), 
514                     "Not enough time for all threads to complete, try increasing the timeout");
515         } catch (InterruptedException ie) {
516             Assertions.fail(ie.getLocalizedMessage());
517         }
518 
519         if (caught.get() != null) {
520             throw caught.get();
521         }
522 
523         if (failedDates.get() != null) {
524             AbsoluteDate[] dates = failedDates.get();
525             StringBuilder builder = new StringBuilder();
526             String eol = System.getProperty("line.separator");
527             builder.append("central = ").append(dates[0]).append(eol);
528             builder.append("step = ").append(step).append(eol);
529             builder.append("neighbors =").append(eol);
530             for (int i = 1; i < dates.length; ++i) {
531                 builder.append("    ").append(dates[i]).append(eol);
532             }
533             Assertions.fail(builder.toString());
534         }
535 
536         return centralDates.size();
537 
538     }
539 
540     private static class Generator implements TimeStampedGenerator<AbsoluteDate> {
541 
542         private final AbsoluteDate earliest;
543         private final AbsoluteDate latest;
544         private final double step;
545 
546         public Generator(final AbsoluteDate earliest, final AbsoluteDate latest, final double step) {
547             this.earliest = earliest;
548             this.latest   = latest;
549             this.step     = step;
550         }
551 
552         public double getStep() {
553             return step;
554         }
555 
556         public List<AbsoluteDate> generate(AbsoluteDate existingDate, AbsoluteDate date) {
557             List<AbsoluteDate> dates = new ArrayList<AbsoluteDate>();
558             if (existingDate == null) {
559                 dates.add(date);
560             } else if (date.compareTo(existingDate) >= 0) {
561                 AbsoluteDate previous = existingDate;
562                 while (date.compareTo(previous) > 0) {
563                     previous = previous.shiftedBy(step);
564                     if (previous.compareTo(earliest) >= 0 && previous.compareTo(latest) <= 0) {
565                         dates.add(dates.size(), previous);
566                     }
567                 }
568             } else {
569                 AbsoluteDate previous = existingDate;
570                 while (date.compareTo(previous) < 0) {
571                     previous = previous.shiftedBy(-step);
572                     if (previous.compareTo(earliest) >= 0 && previous.compareTo(latest) <= 0) {
573                         dates.add(0, previous);
574                     }
575                 }
576             }
577             return dates;
578         }
579 
580     }
581 
582     private interface Mode {
583         List<AbsoluteDate> generateDates(AbsoluteDate[] base, double duration, double step);
584     }
585 
586     private class SequentialMode implements Mode {
587 
588         public List<AbsoluteDate> generateDates(AbsoluteDate[] base, double duration, double step) {
589             List<AbsoluteDate> list = new ArrayList<AbsoluteDate>();
590             for (final AbsoluteDate initial : base) {
591                 for (double dt = 0; dt < duration; dt += step) {
592                     list.add(initial.shiftedBy(dt));
593                 }
594             }
595             return list;
596         }
597 
598     }
599 
600     private class AlternateMode implements Mode {
601 
602         public List<AbsoluteDate> generateDates(AbsoluteDate[] base, double duration, double step) {
603             List<AbsoluteDate> list = new ArrayList<AbsoluteDate>();
604             for (double dt = 0; dt < duration; dt += step) {
605                 for (final AbsoluteDate initial : base) {
606                     list.add(initial.shiftedBy(dt));
607                 }
608             }
609             return list;
610         }
611 
612     }
613 
614     private class RandomMode implements Mode {
615 
616         private RandomGenerator random;
617 
618         public RandomMode(long seed) {
619             random = new Well1024a(seed);
620         }
621 
622         public List<AbsoluteDate> generateDates(AbsoluteDate[] base, double duration, double step) {
623             List<AbsoluteDate> list = new ArrayList<AbsoluteDate>();
624             for (int i = 0; i < base.length * duration / step; ++i) {
625                 int j     = random.nextInt(base.length);
626                 double dt = random.nextDouble() * duration;
627                     list.add(base[j].shiftedBy(dt));
628             }
629             return list;
630         }
631 
632     }
633 
634     @BeforeEach
635     public void setUp() {
636         Utils.setDataRoot("regular-data");
637     }
638 }