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