1   /* Copyright 2022-2025 Thales Alenia Space
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.frames;
18  
19  import org.hipparchus.random.RandomGenerator;
20  import org.hipparchus.random.Well19937a;
21  import org.junit.jupiter.api.AfterEach;
22  import org.junit.jupiter.api.Assertions;
23  import org.junit.jupiter.api.BeforeEach;
24  import org.junit.jupiter.api.Test;
25  import org.orekit.Utils;
26  import org.orekit.time.AbsoluteDate;
27  import org.orekit.utils.Constants;
28  import org.orekit.utils.IERSConventions;
29  
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.concurrent.Callable;
33  import java.util.concurrent.ExecutionException;
34  import java.util.concurrent.ExecutorService;
35  import java.util.concurrent.Executors;
36  import java.util.concurrent.Future;
37  
38  public class CachedTransformProviderTest {
39  
40      private Frame         inertialFrame;
41      private CountingFrame earth1;
42      private CountingFrame earth2;
43  
44      @Test
45      public void testSingleThread() {
46          final CachedTransformProvider cachedTransformProvider = buildCache(20);
47          Assertions.assertSame(earth1,        cachedTransformProvider.getOrigin());
48          Assertions.assertSame(inertialFrame, cachedTransformProvider.getDestination());
49          Assertions.assertEquals(20,  cachedTransformProvider.getCacheSize());
50          final List<AbsoluteDate> dates = generateDates(new Well19937a(0x03fb4b0832dadcbe2L), 50, 5);
51          for (final AbsoluteDate date : dates) {
52              final Transform transform1   = cachedTransformProvider.getTransform(date);
53              final Transform transform2   = earth2.getTransformTo(inertialFrame, date);
54              final Transform backAndForth = new Transform(date, transform1, transform2.getInverse());
55              Assertions.assertEquals(0.0, backAndForth.getRotation().getAngle(), 3.0e-15);
56          }
57          Assertions.assertEquals(10, earth1.count);
58          Assertions.assertEquals(dates.size(), earth2.count);
59      }
60  
61      @Test
62      public void testSingleThreadKinematic() {
63          final CachedTransformProvider cachedTransformProvider = buildCache(20);
64          Assertions.assertSame(earth1,        cachedTransformProvider.getOrigin());
65          Assertions.assertSame(inertialFrame, cachedTransformProvider.getDestination());
66          Assertions.assertEquals(20,  cachedTransformProvider.getCacheSize());
67          final List<AbsoluteDate> dates = generateDates(new Well19937a(0x03fb4b0832dadcbe2L), 50, 5);
68          for (final AbsoluteDate date : dates) {
69              final KinematicTransform transform1   = cachedTransformProvider.getKinematicTransform(date);
70              final KinematicTransform transform2   = earth2.getKinematicTransformTo(inertialFrame, date);
71              final KinematicTransform backAndForth = KinematicTransform.compose(date, transform1, transform2.getInverse());
72              Assertions.assertEquals(0.0, backAndForth.getRotation().getAngle(), 3.0e-15);
73          }
74          Assertions.assertEquals(10, earth1.count);
75          Assertions.assertEquals(dates.size(), earth2.count);
76      }
77  
78      @Test
79      public void testSingleThreadStatic() {
80          final CachedTransformProvider cachedTransformProvider = buildCache(20);
81          Assertions.assertSame(earth1,        cachedTransformProvider.getOrigin());
82          Assertions.assertSame(inertialFrame, cachedTransformProvider.getDestination());
83          Assertions.assertEquals(20,  cachedTransformProvider.getCacheSize());
84          final List<AbsoluteDate> dates = generateDates(new Well19937a(0x03fb4b0832dadcbe2L), 50, 5);
85          for (final AbsoluteDate date : dates) {
86              final StaticTransform transform1   = cachedTransformProvider.getStaticTransform(date);
87              final StaticTransform transform2   = earth2.getStaticTransformTo(inertialFrame, date);
88              final StaticTransform backAndForth = StaticTransform.compose(date, transform1, transform2.getInverse());
89              Assertions.assertEquals(0.0, backAndForth.getRotation().getAngle(), 3.0e-15);
90          }
91          Assertions.assertEquals(10, earth1.count);
92          Assertions.assertEquals(dates.size(), earth2.count);
93      }
94  
95      @Test
96      public void testMultiThread() throws InterruptedException, ExecutionException {
97          final CachedTransformProvider cachedTransformProvider = buildCache(30);
98          Assertions.assertEquals(30,  cachedTransformProvider.getCacheSize());
99          final List<AbsoluteDate> dates = generateDates(new Well19937a(0x7d63ba984c6ae29eL), 300, 10);
100         final List<Callable<Transform>> tasks = new ArrayList<>();
101         for (final AbsoluteDate date : dates) {
102             tasks.add(() -> cachedTransformProvider.getTransform(date));
103         }
104         ExecutorService executorService = Executors.newFixedThreadPool(8);
105         final List<Future<Transform>> futures = executorService.invokeAll(tasks);
106         for (int i = 0; i < dates.size(); i++) {
107             final Transform transform1   = futures.get(i).get();
108             final Transform transform2   = earth2.getTransformTo(inertialFrame, dates.get(i));
109             final Transform backAndForth = new Transform(dates.get(i), transform1, transform2.getInverse());
110             Assertions.assertEquals(0.0, backAndForth.getRotation().getAngle(), 1.0e-14);
111         }
112         Assertions.assertTrue(earth1.count < 50, "this test may randomly fail due to multi-threading non-determinism");
113         Assertions.assertEquals(dates.size(), earth2.count);
114     }
115 
116     @Test
117     public void testExhaust() {
118         final RandomGenerator random = new Well19937a(0x3b18a628c1a8b5e9L);
119         final CachedTransformProvider cachedTransformProvider = buildCache(20);
120         final List<AbsoluteDate> dates = generateDates(random,
121                                                        10 * cachedTransformProvider.getCacheSize(),
122                                                        50 * cachedTransformProvider.getCacheSize());
123 
124         // first batch, without exhausting
125         final List<Transform> firstBatch = new ArrayList<>();
126         for (int i = 0; i < cachedTransformProvider.getCacheSize(); i++) {
127             firstBatch.add(cachedTransformProvider.getTransform(dates.get(i)));
128         }
129         for (int i = 0; i < 1000; i++) {
130             // we should retrieve again and again the already computed instances
131             final int k = random.nextInt(firstBatch.size());
132             Assertions.assertSame(firstBatch.get(k), cachedTransformProvider.getTransform(dates.get(k)));
133         }
134         final Transform t14 = cachedTransformProvider.getTransform(dates.get(14));
135 
136         // now exhaust the instance, except we force entry 14 to remain in the cache by reusing it
137         for (int i = 0; i < dates.size(); i++) {
138             Assertions.assertNotNull(cachedTransformProvider.getTransform(dates.get(dates.size() - 1 - i)));
139             Assertions.assertNotNull(cachedTransformProvider.getTransform(dates.get(14)));
140         }
141 
142         for (int i = 0; i < 100; i++) {
143             // we should get new instances from the first already known dates,
144             // but they should correspond to similar transforms
145             final int       k            = random.nextInt(firstBatch.size());
146             final Transform t            = cachedTransformProvider.getTransform(dates.get(k));
147             final Transform backAndForth = new Transform(dates.get(k), firstBatch.get(k), t.getInverse());
148             if (k != 14) {
149                 Assertions.assertNotSame(firstBatch.get(k), t);
150             }
151             Assertions.assertEquals(0.0, backAndForth.getRotation().getAngle(), 1.0e-20);
152             Assertions.assertEquals(0.0, backAndForth.getCartesian().getPosition().getNorm(), 1.0e-20);
153         }
154 
155         // entry 14 should still be in the cache
156         Assertions.assertSame(t14, cachedTransformProvider.getTransform(dates.get(14)));
157 
158     }
159 
160     @Test
161     public void testExhaustKinematic() {
162         final RandomGenerator random = new Well19937a(0x3b18a628c1a8b5e9L);
163         final CachedTransformProvider cachedTransformProvider = buildCache(20);
164         final List<AbsoluteDate> dates = generateDates(random,
165                                                        10 * cachedTransformProvider.getCacheSize(),
166                                                        50 * cachedTransformProvider.getCacheSize());
167 
168         // first batch, without exhausting
169         final List<KinematicTransform> firstBatch = new ArrayList<>();
170         for (int i = 0; i < cachedTransformProvider.getCacheSize(); i++) {
171             firstBatch.add(cachedTransformProvider.getKinematicTransform(dates.get(i)));
172         }
173         for (int i = 0; i < 1000; i++) {
174             // we should retrieve again and again the already computed instances
175             final int k = random.nextInt(firstBatch.size());
176             Assertions.assertSame(firstBatch.get(k), cachedTransformProvider.getKinematicTransform(dates.get(k)));
177         }
178         final KinematicTransform t14 = cachedTransformProvider.getKinematicTransform(dates.get(14));
179 
180         // now exhaust the instance, except we force entry 14 to remain in the cache by reusing it
181         for (int i = 0; i < dates.size(); i++) {
182             Assertions.assertNotNull(cachedTransformProvider.getKinematicTransform(dates.get(dates.size() - 1 - i)));
183             Assertions.assertNotNull(cachedTransformProvider.getKinematicTransform(dates.get(14)));
184         }
185 
186         for (int i = 0; i < 100; i++) {
187             // we should get new instances from the first already known dates,
188             // but they should correspond to similar transforms
189             final int                k            = random.nextInt(firstBatch.size());
190             final KinematicTransform t            = cachedTransformProvider.getTransform(dates.get(k));
191             final KinematicTransform backAndForth = KinematicTransform.compose(dates.get(k), firstBatch.get(k), t.getInverse());
192             if (k != 14) {
193                 Assertions.assertNotSame(firstBatch.get(k), t);
194             }
195             Assertions.assertEquals(0.0, backAndForth.getRotation().getAngle(), 1.0e-20);
196             Assertions.assertEquals(0.0, backAndForth.getTranslation().getNorm(), 1.0e-20);
197         }
198 
199         // entry 14 should still be in the cache
200         Assertions.assertSame(t14, cachedTransformProvider.getKinematicTransform(dates.get(14)));
201 
202     }
203 
204     @Test
205     public void testExhaustStatic() {
206         final RandomGenerator random = new Well19937a(0x3b18a628c1a8b5e9L);
207         final CachedTransformProvider cachedTransformProvider = buildCache(20);
208         final List<AbsoluteDate> dates = generateDates(random,
209                                                        10 * cachedTransformProvider.getCacheSize(),
210                                                        50 * cachedTransformProvider.getCacheSize());
211 
212         // first batch, without exhausting
213         final List<StaticTransform> firstBatch = new ArrayList<>();
214         for (int i = 0; i < cachedTransformProvider.getCacheSize(); i++) {
215             firstBatch.add(cachedTransformProvider.getStaticTransform(dates.get(i)));
216         }
217         for (int i = 0; i < 1000; i++) {
218             // we should retrieve again and again the already computed instances
219             final int k = random.nextInt(firstBatch.size());
220             Assertions.assertSame(firstBatch.get(k), cachedTransformProvider.getStaticTransform(dates.get(k)));
221         }
222         final StaticTransform t14 = cachedTransformProvider.getStaticTransform(dates.get(14));
223 
224         // now exhaust the instance, except we force entry 14 to remain in the cache by reusing it
225         for (int i = 0; i < dates.size(); i++) {
226             Assertions.assertNotNull(cachedTransformProvider.getStaticTransform(dates.get(dates.size() - 1 - i)));
227             Assertions.assertNotNull(cachedTransformProvider.getStaticTransform(dates.get(14)));
228         }
229 
230         for (int i = 0; i < 100; i++) {
231             // we should get new instances from the first already known dates,
232             // but they should correspond to similar transforms
233             final int             k            = random.nextInt(firstBatch.size());
234             final StaticTransform t            = cachedTransformProvider.getTransform(dates.get(k));
235             final StaticTransform backAndForth = StaticTransform.compose(dates.get(k), firstBatch.get(k), t.getInverse());
236             if (k != 14) {
237                 Assertions.assertNotSame(firstBatch.get(k), t);
238             }
239             Assertions.assertEquals(0.0, backAndForth.getRotation().getAngle(), 1.0e-20);
240             Assertions.assertEquals(0.0, backAndForth.getTranslation().getNorm(), 1.0e-20);
241         }
242 
243         // entry 14 should still be in the cache
244         Assertions.assertSame(t14, cachedTransformProvider.getStaticTransform(dates.get(14)));
245 
246     }
247 
248     private CachedTransformProvider buildCache(final int size) {
249         return new CachedTransformProvider(earth1, inertialFrame,
250                                            d -> earth1.getTransformTo(inertialFrame, d),
251                                            d -> earth1.getKinematicTransformTo(inertialFrame, d),
252                                            d -> earth1.getStaticTransformTo(inertialFrame, d),
253                                            size);
254 
255     }
256 
257     private List<AbsoluteDate> generateDates(final RandomGenerator random, final int total, final int history) {
258         final List<AbsoluteDate> dates = new ArrayList<>(total);
259         for (int i = 0; i < total; i++) {
260             final int index = i - random.nextInt(history);
261             final AbsoluteDate date = index < 0 || index >= dates.size() ?
262                                       AbsoluteDate.ARBITRARY_EPOCH.shiftedBy(Constants.JULIAN_DAY * random.nextDouble()) :
263                                       dates.get(index);
264             dates.add(date);
265         }
266         return dates;
267     }
268 
269     @BeforeEach
270     public void setUp() {
271         Utils.setDataRoot("regular-data");
272         final Frame earthFrame = FramesFactory.getITRF(IERSConventions.IERS_2010, false);
273         this.inertialFrame     = FramesFactory.getEME2000();
274         this.earth1            = new CountingFrame(earthFrame);
275         this.earth2            = new CountingFrame(earthFrame);
276     }
277 
278     @AfterEach
279     public void tearDown() {
280         inertialFrame = null;
281         earth1        = null;
282         earth2        = null;
283     }
284 
285     /** Frame counting transform builds. */
286     private static class CountingFrame extends Frame {
287         int count;
288 
289         CountingFrame(final Frame frame) {
290             super(frame, Transform.IDENTITY, "counting", false);
291             count = 0;
292         }
293 
294         public Transform getTransformTo(final Frame destination, final AbsoluteDate date) {
295             ++count;
296             return super.getTransformTo(destination, date);
297         }
298 
299         public KinematicTransform getKinematicTransformTo(final Frame destination, final AbsoluteDate date) {
300             ++count;
301             return super.getKinematicTransformTo(destination, date);
302         }
303 
304         public StaticTransform getStaticTransformTo(final Frame destination, final AbsoluteDate date) {
305             ++count;
306             return super.getStaticTransformTo(destination, date);
307         }
308 
309     }
310 
311 }