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