1   /* Copyright 2002-2024 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.frames;
18  
19  import java.io.Serializable;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.List;
23  import java.util.Optional;
24  import java.util.function.BiFunction;
25  import java.util.function.Consumer;
26  import java.util.function.Function;
27  import java.util.stream.Stream;
28  
29  import org.hipparchus.CalculusFieldElement;
30  import org.hipparchus.analysis.interpolation.FieldHermiteInterpolator;
31  import org.hipparchus.analysis.interpolation.HermiteInterpolator;
32  import org.hipparchus.util.FastMath;
33  import org.hipparchus.util.MathArrays;
34  import org.orekit.annotation.DefaultDataContext;
35  import org.orekit.data.DataContext;
36  import org.orekit.errors.OrekitException;
37  import org.orekit.errors.OrekitInternalError;
38  import org.orekit.errors.OrekitMessages;
39  import org.orekit.errors.TimeStampedCacheException;
40  import org.orekit.time.AbsoluteDate;
41  import org.orekit.time.FieldAbsoluteDate;
42  import org.orekit.time.TimeScales;
43  import org.orekit.time.TimeStamped;
44  import org.orekit.time.TimeVectorFunction;
45  import org.orekit.utils.Constants;
46  import org.orekit.utils.GenericTimeStampedCache;
47  import org.orekit.utils.IERSConventions;
48  import org.orekit.utils.ImmutableTimeStampedCache;
49  import org.orekit.utils.OrekitConfiguration;
50  import org.orekit.utils.TimeStampedCache;
51  import org.orekit.utils.TimeStampedGenerator;
52  
53  /** This class loads any kind of Earth Orientation Parameter data throughout a large time range.
54   * @author Pascal Parraud
55   * @author Evan Ward
56   */
57  public class EOPHistory implements Serializable {
58  
59      /** Default interpolation degree.
60       * @since 12.0
61       */
62      public static final int DEFAULT_INTERPOLATION_DEGREE = 3;
63  
64      /** Serializable UID. */
65      private static final long serialVersionUID = 20231022L;
66  
67      /** Interpolation degree.
68       * @since 12.0
69       */
70      private final int interpolationDegree;
71  
72      /**
73       * If this history has any EOP data.
74       *
75       * @see #hasDataFor(AbsoluteDate)
76       */
77      private final boolean hasData;
78  
79      /** EOP history entries. */
80      private final transient ImmutableTimeStampedCache<EOPEntry> cache;
81  
82      /** IERS conventions to which EOP refers. */
83      private final IERSConventions conventions;
84  
85      /** Correction to apply to EOP (may be null). */
86      private final transient TimeVectorFunction tidalCorrection;
87  
88      /** Time scales to use when computing corrections. */
89      private final transient TimeScales timeScales;
90  
91      /** Simple constructor.
92       *
93       * <p>This method uses the {@link DataContext#getDefault() default data context}.
94       *
95       * @param conventions IERS conventions to which EOP refers
96       * @param interpolationDegree interpolation degree (must be of the form 4k-1)
97       * @param data the EOP data to use
98       * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
99       * @see #EOPHistory(IERSConventions, int, Collection, boolean, TimeScales)
100      */
101     @DefaultDataContext
102     protected EOPHistory(final IERSConventions conventions,
103                          final int interpolationDegree,
104                          final Collection<? extends EOPEntry> data,
105                          final boolean simpleEOP) {
106         this(conventions, interpolationDegree, data, simpleEOP, DataContext.getDefault().getTimeScales());
107     }
108 
109     /** Simple constructor.
110      * @param conventions IERS conventions to which EOP refers
111      * @param interpolationDegree interpolation degree (must be of the form 4k-1)
112      * @param data the EOP data to use
113      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
114      * @param timeScales to use when computing EOP corrections.
115      * @since 10.1
116      */
117     public EOPHistory(final IERSConventions conventions,
118                       final int interpolationDegree,
119                       final Collection<? extends EOPEntry> data,
120                       final boolean simpleEOP,
121                       final TimeScales timeScales) {
122         this(conventions, interpolationDegree, data,
123              simpleEOP ? null : new CachedCorrection(conventions.getEOPTidalCorrection(timeScales)),
124              timeScales);
125     }
126 
127     /** Simple constructor.
128      * @param conventions IERS conventions to which EOP refers
129      * @param interpolationDegree interpolation degree (must be of the form 4k-1)
130      * @param data the EOP data to use
131      * @param tidalCorrection correction to apply to EOP
132      * @param timeScales to use when computing EOP corrections
133      * @since 12
134      */
135     private EOPHistory(final IERSConventions conventions,
136                        final int interpolationDegree,
137                        final Collection<? extends EOPEntry> data,
138                        final TimeVectorFunction tidalCorrection,
139                        final TimeScales timeScales) {
140 
141         // check interpolation degree is 4k-1
142         final int k = (interpolationDegree + 1) / 4;
143         if (interpolationDegree != 4 * k - 1) {
144             throw new OrekitException(OrekitMessages.WRONG_EOP_INTERPOLATION_DEGREE, interpolationDegree);
145         }
146 
147         this.conventions         = conventions;
148         this.interpolationDegree = interpolationDegree;
149         this.tidalCorrection     = tidalCorrection;
150         this.timeScales          = timeScales;
151         if (data.size() >= 1) {
152             // enough data to interpolate
153             if (missSomeDerivatives(data)) {
154                 // we need to estimate the missing derivatives
155                 final ImmutableTimeStampedCache<EOPEntry> rawCache =
156                                 new ImmutableTimeStampedCache<>(FastMath.min(interpolationDegree + 1, data.size()), data);
157                 final List<EOPEntry>fixedData = new ArrayList<>();
158                 for (final EOPEntry entry : rawCache.getAll()) {
159                     fixedData.add(fixDerivatives(entry, rawCache));
160                 }
161                 cache = new ImmutableTimeStampedCache<>(FastMath.min(interpolationDegree + 1, fixedData.size()), fixedData);
162             } else {
163                 cache = new ImmutableTimeStampedCache<>(FastMath.min(interpolationDegree + 1, data.size()), data);
164             }
165             hasData = true;
166         } else {
167             // not enough data to interpolate -> always use null correction
168             cache   = ImmutableTimeStampedCache.emptyCache();
169             hasData = false;
170         }
171     }
172 
173     /**
174      * Determine if this history uses simplified EOP corrections.
175      *
176      * @return {@code true} if tidal corrections are ignored, {@code false} otherwise.
177      */
178     public boolean isSimpleEop() {
179         return tidalCorrection == null;
180     }
181 
182     /** Get interpolation degree.
183      * @return interpolation degree
184      * @since 12.0
185      */
186     public int getInterpolationDegree() {
187         return interpolationDegree;
188     }
189 
190     /**
191      * Get the time scales used in computing EOP corrections.
192      *
193      * @return set of time scales.
194      * @since 10.1
195      */
196     public TimeScales getTimeScales() {
197         return timeScales;
198     }
199 
200     /** Get version of the instance that does not cache tidal correction.
201      * @return version of the instance that does not cache tidal correction
202      * @since 12.0
203      */
204     public EOPHistory getEOPHistoryWithoutCachedTidalCorrection() {
205         return new EOPHistory(conventions, interpolationDegree, getEntries(),
206                               conventions.getEOPTidalCorrection(timeScales),
207                               timeScales);
208     }
209 
210     /** Check if the instance caches tidal corrections.
211      * @return true if the instance caches tidal corrections
212      * @since 12.0
213      */
214     public boolean cachesTidalCorrection() {
215         return tidalCorrection instanceof CachedCorrection;
216     }
217 
218     /** Get the IERS conventions to which these EOP apply.
219      * @return IERS conventions to which these EOP apply
220      */
221     public IERSConventions getConventions() {
222         return conventions;
223     }
224 
225     /** Get the date of the first available Earth Orientation Parameters.
226      * @return the start date of the available data
227      */
228     public AbsoluteDate getStartDate() {
229         return this.cache.getEarliest().getDate();
230     }
231 
232     /** Get the date of the last available Earth Orientation Parameters.
233      * @return the end date of the available data
234      */
235     public AbsoluteDate getEndDate() {
236         return this.cache.getLatest().getDate();
237     }
238 
239     /** Get the UT1-UTC value.
240      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
241      * @param date date at which the value is desired
242      * @return UT1-UTC in seconds (0 if date is outside covered range)
243      */
244     public double getUT1MinusUTC(final AbsoluteDate date) {
245 
246         //check if there is data for date
247         if (!this.hasDataFor(date)) {
248             // no EOP data available for this date, we use a default 0.0 offset
249             return (tidalCorrection == null) ? 0.0 : tidalCorrection.value(date)[2];
250         }
251 
252         // we have EOP data -> interpolate offset
253         try {
254             final DUT1Interpolator interpolator = new DUT1Interpolator(date);
255             getNeighbors(date, interpolationDegree + 1).forEach(interpolator);
256             double interpolated = interpolator.getInterpolated();
257             if (tidalCorrection != null) {
258                 interpolated += tidalCorrection.value(date)[2];
259             }
260             return interpolated;
261         } catch (TimeStampedCacheException tce) {
262             //this should not happen because of date check above
263             throw new OrekitInternalError(tce);
264         }
265 
266     }
267 
268     /** Get the UT1-UTC value.
269      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
270      * @param date date at which the value is desired
271      * @param <T> type of the field elements
272      * @return UT1-UTC in seconds (0 if date is outside covered range)
273      * @since 9.0
274      */
275     public <T extends CalculusFieldElement<T>> T getUT1MinusUTC(final FieldAbsoluteDate<T> date) {
276 
277         //check if there is data for date
278         final AbsoluteDate absDate = date.toAbsoluteDate();
279         if (!this.hasDataFor(absDate)) {
280             // no EOP data available for this date, we use a default 0.0 offset
281             return (tidalCorrection == null) ? date.getField().getZero() : tidalCorrection.value(date)[2];
282         }
283 
284         // we have EOP data -> interpolate offset
285         try {
286             final FieldDUT1Interpolator<T> interpolator = new FieldDUT1Interpolator<>(date, absDate);
287             getNeighbors(absDate, interpolationDegree + 1).forEach(interpolator);
288             T interpolated = interpolator.getInterpolated();
289             if (tidalCorrection != null) {
290                 interpolated = interpolated.add(tidalCorrection.value(date)[2]);
291             }
292             return interpolated;
293         } catch (TimeStampedCacheException tce) {
294             //this should not happen because of date check above
295             throw new OrekitInternalError(tce);
296         }
297 
298     }
299 
300     /** Local class for DUT1 interpolation, crossing leaps safely. */
301     private static class DUT1Interpolator implements Consumer<EOPEntry> {
302 
303         /** DUT at first entry. */
304         private double firstDUT;
305 
306         /** Indicator for dates just before a leap occurring during the interpolation sample. */
307         private boolean beforeLeap;
308 
309         /** Interpolator to use. */
310         private final HermiteInterpolator interpolator;
311 
312         /** Interpolation date. */
313         private AbsoluteDate date;
314 
315         /** Simple constructor.
316          * @param date interpolation date
317          */
318         DUT1Interpolator(final AbsoluteDate date) {
319             this.firstDUT     = Double.NaN;
320             this.beforeLeap   = true;
321             this.interpolator = new HermiteInterpolator();
322             this.date         = date;
323         }
324 
325         /** {@inheritDoc} */
326         @Override
327         public void accept(final EOPEntry neighbor) {
328             if (Double.isNaN(firstDUT)) {
329                 firstDUT = neighbor.getUT1MinusUTC();
330             }
331             final double dut;
332             if (neighbor.getUT1MinusUTC() - firstDUT > 0.9) {
333                 // there was a leap second between the entries
334                 dut = neighbor.getUT1MinusUTC() - 1.0;
335                 // UTCScale considers the discontinuity to occur at the start of the leap
336                 // second so this code must use the same convention. EOP entries are time
337                 // stamped at midnight UTC so 1 second before is the start of the leap
338                 // second.
339                 if (neighbor.getDate().shiftedBy(-1).compareTo(date) <= 0) {
340                     beforeLeap = false;
341                 }
342             } else {
343                 dut = neighbor.getUT1MinusUTC();
344             }
345             interpolator.addSamplePoint(neighbor.getDate().durationFrom(date),
346                                         new double[] {
347                                             dut
348                                         });
349         }
350 
351         /** Get the interpolated value.
352          * @return interpolated value
353          */
354         public double getInterpolated() {
355             final double interpolated = interpolator.value(0)[0];
356             return beforeLeap ? interpolated : interpolated + 1.0;
357         }
358 
359     }
360 
361     /** Local class for DUT1 interpolation, crossing leaps safely. */
362     private static class FieldDUT1Interpolator<T extends CalculusFieldElement<T>> implements Consumer<EOPEntry> {
363 
364         /** DUT at first entry. */
365         private double firstDUT;
366 
367         /** Indicator for dates just before a leap occurring during the interpolation sample. */
368         private boolean beforeLeap;
369 
370         /** Interpolator to use. */
371         private final FieldHermiteInterpolator<T> interpolator;
372 
373         /** Interpolation date. */
374         private FieldAbsoluteDate<T> date;
375 
376         /** Interpolation date. */
377         private AbsoluteDate absDate;
378 
379         /** Simple constructor.
380          * @param date interpolation date
381          * @param absDate interpolation date
382          */
383         FieldDUT1Interpolator(final FieldAbsoluteDate<T> date, final AbsoluteDate absDate) {
384             this.firstDUT     = Double.NaN;
385             this.beforeLeap   = true;
386             this.interpolator = new FieldHermiteInterpolator<>();
387             this.date         = date;
388             this.absDate      = absDate;
389         }
390 
391         /** {@inheritDoc} */
392         @Override
393         public void accept(final EOPEntry neighbor) {
394             if (Double.isNaN(firstDUT)) {
395                 firstDUT = neighbor.getUT1MinusUTC();
396             }
397             final double dut;
398             if (neighbor.getUT1MinusUTC() - firstDUT > 0.9) {
399                 // there was a leap second between the entries
400                 dut = neighbor.getUT1MinusUTC() - 1.0;
401                 if (neighbor.getDate().compareTo(absDate) <= 0) {
402                     beforeLeap = false;
403                 }
404             } else {
405                 dut = neighbor.getUT1MinusUTC();
406             }
407             final T[] array = MathArrays.buildArray(date.getField(), 1);
408             array[0] = date.getField().getZero().newInstance(dut);
409             interpolator.addSamplePoint(date.durationFrom(neighbor.getDate()).negate(),
410                                         array);
411         }
412 
413         /** Get the interpolated value.
414          * @return interpolated value
415          */
416         public T getInterpolated() {
417             final T interpolated = interpolator.value(date.getField().getZero())[0];
418             return beforeLeap ? interpolated : interpolated.add(1.0);
419         }
420 
421     }
422 
423     /**
424      * Get the entries surrounding a central date.
425      * <p>
426      * See {@link #hasDataFor(AbsoluteDate)} to determine if the cache has data
427      * for {@code central} without throwing an exception.
428      *
429      * @param central central date
430      * @param n number of neighbors
431      * @return array of cached entries surrounding specified date
432      * @since 12.0
433      */
434     protected Stream<EOPEntry> getNeighbors(final AbsoluteDate central, final int n) {
435         return cache.getNeighbors(central, n);
436     }
437 
438     /** Get the LoD (Length of Day) value.
439      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
440      * @param date date at which the value is desired
441      * @return LoD in seconds (0 if date is outside covered range)
442      */
443     public double getLOD(final AbsoluteDate date) {
444 
445         // check if there is data for date
446         if (!this.hasDataFor(date)) {
447             // no EOP data available for this date, we use a default null correction
448             return (tidalCorrection == null) ? 0.0 : tidalCorrection.value(date)[3];
449         }
450 
451         // we have EOP data for date -> interpolate correction
452         double interpolated = interpolate(date, entry -> entry.getLOD());
453         if (tidalCorrection != null) {
454             interpolated += tidalCorrection.value(date)[3];
455         }
456         return interpolated;
457 
458     }
459 
460     /** Get the LoD (Length of Day) value.
461      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
462      * @param date date at which the value is desired
463      * @param <T> type of the filed elements
464      * @return LoD in seconds (0 if date is outside covered range)
465      * @since 9.0
466      */
467     public <T extends CalculusFieldElement<T>> T getLOD(final FieldAbsoluteDate<T> date) {
468 
469         final AbsoluteDate aDate = date.toAbsoluteDate();
470 
471         // check if there is data for date
472         if (!this.hasDataFor(aDate)) {
473             // no EOP data available for this date, we use a default null correction
474             return (tidalCorrection == null) ? date.getField().getZero() : tidalCorrection.value(date)[3];
475         }
476 
477         // we have EOP data for date -> interpolate correction
478         T interpolated = interpolate(date, aDate, entry -> entry.getLOD());
479         if (tidalCorrection != null) {
480             interpolated = interpolated.add(tidalCorrection.value(date)[3]);
481         }
482 
483         return interpolated;
484 
485     }
486 
487     /** Get the pole IERS Reference Pole correction.
488      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
489      * @param date date at which the correction is desired
490      * @return pole correction ({@link PoleCorrection#NULL_CORRECTION
491      * PoleCorrection.NULL_CORRECTION} if date is outside covered range)
492      */
493     public PoleCorrection getPoleCorrection(final AbsoluteDate date) {
494 
495         // check if there is data for date
496         if (!this.hasDataFor(date)) {
497             // no EOP data available for this date, we use a default null correction
498             if (tidalCorrection == null) {
499                 return PoleCorrection.NULL_CORRECTION;
500             } else {
501                 final double[] correction = tidalCorrection.value(date);
502                 return new PoleCorrection(correction[0], correction[1]);
503             }
504         }
505 
506         // we have EOP data for date -> interpolate correction
507         final double[] interpolated = interpolate(date,
508                                                   entry -> entry.getX(),     entry -> entry.getY(),
509                                                   entry -> entry.getXRate(), entry -> entry.getYRate());
510         if (tidalCorrection != null) {
511             final double[] correction = tidalCorrection.value(date);
512             interpolated[0] += correction[0];
513             interpolated[1] += correction[1];
514         }
515         return new PoleCorrection(interpolated[0], interpolated[1]);
516 
517     }
518 
519     /** Get the pole IERS Reference Pole correction.
520      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
521      * @param date date at which the correction is desired
522      * @param <T> type of the field elements
523      * @return pole correction ({@link PoleCorrection#NULL_CORRECTION
524      * PoleCorrection.NULL_CORRECTION} if date is outside covered range)
525      */
526     public <T extends CalculusFieldElement<T>> FieldPoleCorrection<T> getPoleCorrection(final FieldAbsoluteDate<T> date) {
527 
528         final AbsoluteDate aDate = date.toAbsoluteDate();
529 
530         // check if there is data for date
531         if (!this.hasDataFor(aDate)) {
532             // no EOP data available for this date, we use a default null correction
533             if (tidalCorrection == null) {
534                 return new FieldPoleCorrection<>(date.getField().getZero(), date.getField().getZero());
535             } else {
536                 final T[] correction = tidalCorrection.value(date);
537                 return new FieldPoleCorrection<>(correction[0], correction[1]);
538             }
539         }
540 
541         // we have EOP data for date -> interpolate correction
542         final T[] interpolated = interpolate(date, aDate, entry -> entry.getX(), entry -> entry.getY());
543         if (tidalCorrection != null) {
544             final T[] correction = tidalCorrection.value(date);
545             interpolated[0] = interpolated[0].add(correction[0]);
546             interpolated[1] = interpolated[1].add(correction[1]);
547         }
548         return new FieldPoleCorrection<>(interpolated[0], interpolated[1]);
549 
550     }
551 
552     /** Get the correction to the nutation parameters for equinox-based paradigm.
553      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
554      * @param date date at which the correction is desired
555      * @return nutation correction in longitude ΔΨ and in obliquity Δε
556      * (zero if date is outside covered range)
557      */
558     public double[] getEquinoxNutationCorrection(final AbsoluteDate date) {
559 
560         // check if there is data for date
561         if (!this.hasDataFor(date)) {
562             // no EOP data available for this date, we use a default null correction
563             return new double[2];
564         }
565 
566         // we have EOP data for date -> interpolate correction
567         return interpolate(date, entry -> entry.getDdPsi(), entry -> entry.getDdEps());
568 
569     }
570 
571     /** Get the correction to the nutation parameters for equinox-based paradigm.
572      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
573      * @param date date at which the correction is desired
574      * @param <T> type of the field elements
575      * @return nutation correction in longitude ΔΨ and in obliquity Δε
576      * (zero if date is outside covered range)
577      */
578     public <T extends CalculusFieldElement<T>> T[] getEquinoxNutationCorrection(final FieldAbsoluteDate<T> date) {
579 
580         final AbsoluteDate aDate = date.toAbsoluteDate();
581 
582         // check if there is data for date
583         if (!this.hasDataFor(aDate)) {
584             // no EOP data available for this date, we use a default null correction
585             return MathArrays.buildArray(date.getField(), 2);
586         }
587 
588         // we have EOP data for date -> interpolate correction
589         return interpolate(date, aDate, entry -> entry.getDdPsi(), entry -> entry.getDdEps());
590 
591     }
592 
593     /** Get the correction to the nutation parameters for Non-Rotating Origin paradigm.
594      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
595      * @param date date at which the correction is desired
596      * @return nutation correction in Celestial Intermediate Pole coordinates
597      * δX and δY (zero if date is outside covered range)
598      */
599     public double[] getNonRotatinOriginNutationCorrection(final AbsoluteDate date) {
600 
601         // check if there is data for date
602         if (!this.hasDataFor(date)) {
603             // no EOP data available for this date, we use a default null correction
604             return new double[2];
605         }
606 
607         // we have EOP data for date -> interpolate correction
608         return interpolate(date, entry -> entry.getDx(), entry -> entry.getDy());
609 
610     }
611 
612     /** Get the correction to the nutation parameters for Non-Rotating Origin paradigm.
613      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
614      * @param date date at which the correction is desired
615      * @param <T> type of the filed elements
616      * @return nutation correction in Celestial Intermediate Pole coordinates
617      * δX and δY (zero if date is outside covered range)
618      */
619     public <T extends CalculusFieldElement<T>> T[] getNonRotatinOriginNutationCorrection(final FieldAbsoluteDate<T> date) {
620 
621         final AbsoluteDate aDate = date.toAbsoluteDate();
622 
623         // check if there is data for date
624         if (!this.hasDataFor(aDate)) {
625             // no EOP data available for this date, we use a default null correction
626             return MathArrays.buildArray(date.getField(), 2);
627         }
628 
629         // we have EOP data for date -> interpolate correction
630         return interpolate(date, aDate, entry -> entry.getDx(), entry -> entry.getDy());
631 
632     }
633 
634     /** Get the ITRF version.
635      * @param date date at which the value is desired
636      * @return ITRF version of the EOP covering the specified date
637      * @since 9.2
638      */
639     public ITRFVersion getITRFVersion(final AbsoluteDate date) {
640 
641         // check if there is data for date
642         if (!this.hasDataFor(date)) {
643             // no EOP data available for this date, we use a default ITRF 2014
644             return ITRFVersion.ITRF_2014;
645         }
646 
647         try {
648             // we have EOP data for date
649             final Optional<EOPEntry> first = getNeighbors(date, 1).findFirst();
650             return first.isPresent() ? first.get().getITRFType() : ITRFVersion.ITRF_2014;
651 
652         } catch (TimeStampedCacheException tce) {
653             // this should not happen because of date check performed at start
654             throw new OrekitInternalError(tce);
655         }
656 
657     }
658 
659     /** Check Earth orientation parameters continuity.
660      * @param maxGap maximal allowed gap between entries (in seconds)
661      */
662     public void checkEOPContinuity(final double maxGap) {
663         TimeStamped preceding = null;
664         for (final TimeStamped current : this.cache.getAll()) {
665 
666             // compare the dates of preceding and current entries
667             if (preceding != null && (current.getDate().durationFrom(preceding.getDate())) > maxGap) {
668                 throw new OrekitException(OrekitMessages.MISSING_EARTH_ORIENTATION_PARAMETERS_BETWEEN_DATES_GAP,
669                                           preceding.getDate(), current.getDate(),
670                                           current.getDate().durationFrom(preceding.getDate()));
671             }
672 
673             // prepare next iteration
674             preceding = current;
675 
676         }
677     }
678 
679     /**
680      * Check if the cache has data for the given date using
681      * {@link #getStartDate()} and {@link #getEndDate()}.
682      *
683      * @param date the requested date
684      * @return true if the {@link #cache} has data for the requested date, false
685      *         otherwise.
686      */
687     protected boolean hasDataFor(final AbsoluteDate date) {
688         /*
689          * when there is no EOP data, short circuit getStartDate, which will
690          * throw an exception
691          */
692         return this.hasData && this.getStartDate().compareTo(date) <= 0 &&
693                date.compareTo(this.getEndDate()) <= 0;
694     }
695 
696     /** Get a non-modifiable view of the EOP entries.
697      * @return non-modifiable view of the EOP entries
698      */
699     public List<EOPEntry> getEntries() {
700         return cache.getAll();
701     }
702 
703     /** Interpolate a single EOP component.
704      * <p>
705      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
706      * </p>
707      * @param date interpolation date
708      * @param selector selector for EOP entry component
709      * @return interpolated value
710      */
711     private double interpolate(final AbsoluteDate date, final Function<EOPEntry, Double> selector) {
712         try {
713             final HermiteInterpolator interpolator = new HermiteInterpolator();
714             getNeighbors(date, interpolationDegree + 1).
715                 forEach(entry -> interpolator.addSamplePoint(entry.getDate().durationFrom(date),
716                                                              new double[] {
717                                                                  selector.apply(entry)
718                                                              }));
719             return interpolator.value(0)[0];
720         } catch (TimeStampedCacheException tce) {
721             // this should not happen because of date check performed by caller
722             throw new OrekitInternalError(tce);
723         }
724     }
725 
726     /** Interpolate a single EOP component.
727      * <p>
728      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
729      * </p>
730      * @param date interpolation date
731      * @param aDate interpolation date, as an {@link AbsoluteDate}
732      * @param selector selector for EOP entry component
733      * @param <T> type of the field elements
734      * @return interpolated value
735      */
736     private <T extends CalculusFieldElement<T>> T interpolate(final FieldAbsoluteDate<T> date,
737                                                               final AbsoluteDate aDate,
738                                                               final Function<EOPEntry, Double> selector) {
739         try {
740             final FieldHermiteInterpolator<T> interpolator = new FieldHermiteInterpolator<>();
741             final T[] y = MathArrays.buildArray(date.getField(), 1);
742             final T zero = date.getField().getZero();
743             final FieldAbsoluteDate<T> central = new FieldAbsoluteDate<>(aDate, zero); // here, we attempt to get a constant date,
744                                                                                        // for example removing derivatives
745                                                                                        // if T was DerivativeStructure
746             getNeighbors(aDate, interpolationDegree + 1).
747                 forEach(entry -> {
748                     y[0] = zero.newInstance(selector.apply(entry));
749                     interpolator.addSamplePoint(central.durationFrom(entry.getDate()).negate(), y);
750                 });
751             return interpolator.value(date.durationFrom(central))[0]; // here, we introduce derivatives again (in DerivativeStructure case)
752         } catch (TimeStampedCacheException tce) {
753             // this should not happen because of date check performed by caller
754             throw new OrekitInternalError(tce);
755         }
756     }
757 
758     /** Interpolate two EOP components.
759      * <p>
760      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
761      * </p>
762      * @param date interpolation date
763      * @param selector1 selector for first EOP entry component
764      * @param selector2 selector for second EOP entry component
765      * @return interpolated value
766      */
767     private double[] interpolate(final AbsoluteDate date,
768                                  final Function<EOPEntry, Double> selector1,
769                                  final Function<EOPEntry, Double> selector2) {
770         try {
771             final HermiteInterpolator interpolator = new HermiteInterpolator();
772             getNeighbors(date, interpolationDegree + 1).
773                 forEach(entry -> interpolator.addSamplePoint(entry.getDate().durationFrom(date),
774                                                              new double[] {
775                                                                  selector1.apply(entry),
776                                                                  selector2.apply(entry)
777                                                              }));
778             return interpolator.value(0);
779         } catch (TimeStampedCacheException tce) {
780             // this should not happen because of date check performed by caller
781             throw new OrekitInternalError(tce);
782         }
783     }
784 
785     /** Interpolate two EOP components.
786      * <p>
787      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
788      * </p>
789      * @param date interpolation date
790      * @param selector1 selector for first EOP entry component
791      * @param selector2 selector for second EOP entry component
792      * @param selector1Rate selector for first EOP entry component rate
793      * @param selector2Rate selector for second EOP entry component rate
794      * @return interpolated value
795      * @since 12.0
796      */
797     private double[] interpolate(final AbsoluteDate date,
798                                  final Function<EOPEntry, Double> selector1,
799                                  final Function<EOPEntry, Double> selector2,
800                                  final Function<EOPEntry, Double> selector1Rate,
801                                  final Function<EOPEntry, Double> selector2Rate) {
802         try {
803             final HermiteInterpolator interpolator = new HermiteInterpolator();
804             getNeighbors(date, (interpolationDegree + 1) / 2).
805                 forEach(entry -> interpolator.addSamplePoint(entry.getDate().durationFrom(date),
806                                                              new double[] {
807                                                                  selector1.apply(entry),
808                                                                  selector2.apply(entry)
809                                                              },
810                                                              new double[] {
811                                                                  selector1Rate.apply(entry),
812                                                                  selector2Rate.apply(entry)
813                                                              }));
814             return interpolator.value(0);
815         } catch (TimeStampedCacheException tce) {
816             // this should not happen because of date check performed by caller
817             throw new OrekitInternalError(tce);
818         }
819     }
820 
821     /** Interpolate two EOP components.
822      * <p>
823      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
824      * </p>
825      * @param date interpolation date
826      * @param aDate interpolation date, as an {@link AbsoluteDate}
827      * @param selector1 selector for first EOP entry component
828      * @param selector2 selector for second EOP entry component
829      * @param <T> type of the field elements
830      * @return interpolated value
831      */
832     private <T extends CalculusFieldElement<T>> T[] interpolate(final FieldAbsoluteDate<T> date,
833                                                                 final AbsoluteDate aDate,
834                                                                 final Function<EOPEntry, Double> selector1,
835                                                                 final Function<EOPEntry, Double> selector2) {
836         try {
837             final FieldHermiteInterpolator<T> interpolator = new FieldHermiteInterpolator<>();
838             final T[] y = MathArrays.buildArray(date.getField(), 2);
839             final T zero = date.getField().getZero();
840             final FieldAbsoluteDate<T> central = new FieldAbsoluteDate<>(aDate, zero); // here, we attempt to get a constant date,
841                                                                                        // for example removing derivatives
842                                                                                        // if T was DerivativeStructure
843             getNeighbors(aDate, interpolationDegree + 1).
844                 forEach(entry -> {
845                     y[0] = zero.newInstance(selector1.apply(entry));
846                     y[1] = zero.newInstance(selector2.apply(entry));
847                     interpolator.addSamplePoint(central.durationFrom(entry.getDate()).negate(), y);
848                 });
849             return interpolator.value(date.durationFrom(central)); // here, we introduce derivatives again (in DerivativeStructure case)
850         } catch (TimeStampedCacheException tce) {
851             // this should not happen because of date check performed by caller
852             throw new OrekitInternalError(tce);
853         }
854     }
855 
856     /** Check if some derivatives are missing.
857      * @param raw raw EOP history
858      * @return true if some derivatives are missing
859      * @since 12.0
860      */
861     private boolean missSomeDerivatives(final Collection<? extends EOPEntry> raw) {
862         for (final EOPEntry entry : raw) {
863             if (Double.isNaN(entry.getLOD() + entry.getXRate() + entry.getYRate())) {
864                 return true;
865             }
866         }
867         return false;
868     }
869 
870     /** Fix missing derivatives.
871      * @param entry EOP entry to fix
872      * @param rawCache raw EOP history cache
873      * @return fixed entry
874      * @since 12.0
875      */
876     private EOPEntry fixDerivatives(final EOPEntry entry, final ImmutableTimeStampedCache<EOPEntry> rawCache) {
877 
878         // helper function to differentiate some EOP parameters
879         final BiFunction<EOPEntry, Function<EOPEntry, Double>, Double> differentiator =
880                         (e, selector) -> {
881                             final HermiteInterpolator interpolator = new HermiteInterpolator();
882                             rawCache.getNeighbors(e.getDate()).
883                                 forEach(f -> interpolator.addSamplePoint(f.getDate().durationFrom(e.getDate()),
884                                                                          new double[] {
885                                                                              selector.apply(f)
886                                                                          }));
887                             return interpolator.derivatives(0.0, 1)[1][0];
888                         };
889 
890         if (Double.isNaN(entry.getLOD() + entry.getXRate() + entry.getYRate())) {
891             final double lod   = Double.isNaN(entry.getLOD()) ?
892                                  -differentiator.apply(entry, e -> e.getUT1MinusUTC()) :
893                                  entry.getLOD();
894             final double xRate = Double.isNaN(entry.getXRate()) ?
895                                  differentiator.apply(entry, e -> e.getX()) :
896                                  entry.getXRate();
897             final double yRate = Double.isNaN(entry.getYRate()) ?
898                                  differentiator.apply(entry, e -> e.getY()) :
899                                  entry.getYRate();
900             return new EOPEntry(entry.getMjd(),
901                                 entry.getUT1MinusUTC(), lod,
902                                 entry.getX(), entry.getY(), xRate, yRate,
903                                 entry.getDdPsi(), entry.getDdEps(),
904                                 entry.getDx(), entry.getDy(),
905                                 entry.getITRFType(), entry.getDate());
906         } else {
907             // the entry already has all derivatives
908             return entry;
909         }
910     }
911 
912     /** Replace the instance with a data transfer object for serialization.
913      * @return data transfer object that will be serialized
914      */
915     @DefaultDataContext
916     private Object writeReplace() {
917         return new DataTransferObject(conventions, interpolationDegree, getEntries(), tidalCorrection == null);
918     }
919 
920     /** Internal class used only for serialization. */
921     @DefaultDataContext
922     private static class DataTransferObject implements Serializable {
923 
924         /** Serializable UID. */
925         private static final long serialVersionUID = 20231122L;
926 
927         /** IERS conventions. */
928         private final IERSConventions conventions;
929 
930         /** Interpolation degree.
931          * @since 12.0
932          */
933         private final int interpolationDegree;
934 
935         /** EOP entries. */
936         private final List<EOPEntry> entries;
937 
938         /** Indicator for simple interpolation without tidal effects. */
939         private final boolean simpleEOP;
940 
941         /** Simple constructor.
942          * @param conventions IERS conventions to which EOP refers
943          * @param interpolationDegree interpolation degree (must be of the form 4k-1)
944          * @param entries the EOP data to use
945          * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
946          * @since 12.0
947          */
948         DataTransferObject(final IERSConventions conventions,
949                            final int interpolationDegree,
950                            final List<EOPEntry> entries,
951                            final boolean simpleEOP) {
952             this.conventions         = conventions;
953             this.interpolationDegree = interpolationDegree;
954             this.entries             = entries;
955             this.simpleEOP           = simpleEOP;
956         }
957 
958         /** Replace the deserialized data transfer object with a {@link EOPHistory}.
959          * @return replacement {@link EOPHistory}
960          */
961         private Object readResolve() {
962             try {
963                 return new EOPHistory(conventions, interpolationDegree, entries, simpleEOP);
964             } catch (OrekitException oe) {
965                 throw new OrekitInternalError(oe);
966             }
967         }
968 
969     }
970 
971     /** Internal class for caching tidal correction. */
972     private static class TidalCorrectionEntry implements TimeStamped {
973 
974         /** Entry date. */
975         private final AbsoluteDate date;
976 
977         /** Correction. */
978         private final double[] correction;
979 
980         /** Simple constructor.
981          * @param date entry date
982          * @param correction correction on the EOP parameters (xp, yp, ut1, lod)
983          */
984         TidalCorrectionEntry(final AbsoluteDate date, final double[] correction) {
985             this.date       = date;
986             this.correction = correction;
987         }
988 
989         /** {@inheritDoc} */
990         @Override
991         public AbsoluteDate getDate() {
992             return date;
993         }
994 
995     }
996 
997     /** Local generator for thread-safe cache. */
998     private static class CachedCorrection
999         implements TimeVectorFunction, TimeStampedGenerator<TidalCorrectionEntry> {
1000 
1001         /** Correction to apply to EOP (may be null). */
1002         private final TimeVectorFunction tidalCorrection;
1003 
1004         /** Step between generated entries. */
1005         private final double step;
1006 
1007         /** Tidal corrections entries cache. */
1008         private final TimeStampedCache<TidalCorrectionEntry> cache;
1009 
1010         /** Simple constructor.
1011          * @param tidalCorrection function computing the tidal correction
1012          */
1013         CachedCorrection(final TimeVectorFunction tidalCorrection) {
1014             this.step            = 60 * 60;
1015             this.tidalCorrection = tidalCorrection;
1016             this.cache           =
1017                 new GenericTimeStampedCache<TidalCorrectionEntry>(8,
1018                                                                   OrekitConfiguration.getCacheSlotsNumber(),
1019                                                                   Constants.JULIAN_DAY * 30,
1020                                                                   Constants.JULIAN_DAY,
1021                                                                   this);
1022         }
1023 
1024         /** {@inheritDoc} */
1025         @Override
1026         public double[] value(final AbsoluteDate date) {
1027             try {
1028                 // set up an interpolator
1029                 final HermiteInterpolator interpolator = new HermiteInterpolator();
1030                 cache.getNeighbors(date).forEach(entry -> interpolator.addSamplePoint(entry.date.durationFrom(date), entry.correction));
1031 
1032                 // interpolate to specified date
1033                 return interpolator.value(0.0);
1034             } catch (TimeStampedCacheException tsce) {
1035                 // this should never happen
1036                 throw new OrekitInternalError(tsce);
1037             }
1038         }
1039 
1040         /** {@inheritDoc} */
1041         @Override
1042         public <T extends CalculusFieldElement<T>> T[] value(final FieldAbsoluteDate<T> date) {
1043             try {
1044 
1045                 final AbsoluteDate aDate = date.toAbsoluteDate();
1046 
1047                 final FieldHermiteInterpolator<T> interpolator = new FieldHermiteInterpolator<>();
1048                 final T[] y = MathArrays.buildArray(date.getField(), 4);
1049                 final T zero = date.getField().getZero();
1050                 final FieldAbsoluteDate<T> central = new FieldAbsoluteDate<>(aDate, zero); // here, we attempt to get a constant date,
1051                                                                                            // for example removing derivatives
1052                                                                                            // if T was DerivativeStructure
1053                 cache.getNeighbors(aDate).forEach(entry -> {
1054                     for (int i = 0; i < y.length; ++i) {
1055                         y[i] = zero.newInstance(entry.correction[i]);
1056                     }
1057                     interpolator.addSamplePoint(central.durationFrom(entry.getDate()).negate(), y);
1058                 });
1059 
1060                 // interpolate to specified date
1061                 return interpolator.value(date.durationFrom(central)); // here, we introduce derivatives again (in DerivativeStructure case)
1062 
1063             } catch (TimeStampedCacheException tsce) {
1064                 // this should never happen
1065                 throw new OrekitInternalError(tsce);
1066             }
1067         }
1068 
1069         /** {@inheritDoc} */
1070         @Override
1071         public List<TidalCorrectionEntry> generate(final AbsoluteDate existingDate, final AbsoluteDate date) {
1072 
1073             final List<TidalCorrectionEntry> generated = new ArrayList<TidalCorrectionEntry>();
1074 
1075             if (existingDate == null) {
1076 
1077                 // no prior existing entries, just generate a first set
1078                 for (int i = -cache.getMaxNeighborsSize() / 2; generated.size() < cache.getMaxNeighborsSize(); ++i) {
1079                     final AbsoluteDate t = date.shiftedBy(i * step);
1080                     generated.add(new TidalCorrectionEntry(t, tidalCorrection.value(t)));
1081                 }
1082 
1083             } else {
1084 
1085                 // some entries have already been generated
1086                 // add the missing ones up to specified date
1087 
1088                 AbsoluteDate t = existingDate;
1089                 if (date.compareTo(t) > 0) {
1090                     // forward generation
1091                     do {
1092                         t = t.shiftedBy(step);
1093                         generated.add(new TidalCorrectionEntry(t, tidalCorrection.value(t)));
1094                     } while (t.compareTo(date) <= 0);
1095                 } else {
1096                     // backward generation
1097                     do {
1098                         t = t.shiftedBy(-step);
1099                         generated.add(0, new TidalCorrectionEntry(t, tidalCorrection.value(t)));
1100                     } while (t.compareTo(date) >= 0);
1101                 }
1102             }
1103 
1104             // return the generated transforms
1105             return generated;
1106 
1107         }
1108     }
1109 
1110 }