EOPHistory.java

  1. /* Copyright 2002-2025 CS GROUP
  2.  * Licensed to CS GROUP (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.frames;

  18. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.List;
  21. import java.util.Optional;
  22. import java.util.function.BiFunction;
  23. import java.util.function.Consumer;
  24. import java.util.function.Function;
  25. import java.util.stream.Stream;

  26. import org.hipparchus.CalculusFieldElement;
  27. import org.hipparchus.analysis.interpolation.FieldHermiteInterpolator;
  28. import org.hipparchus.analysis.interpolation.HermiteInterpolator;
  29. import org.hipparchus.util.FastMath;
  30. import org.hipparchus.util.MathArrays;
  31. import org.orekit.annotation.DefaultDataContext;
  32. import org.orekit.data.DataContext;
  33. import org.orekit.errors.OrekitException;
  34. import org.orekit.errors.OrekitInternalError;
  35. import org.orekit.errors.OrekitMessages;
  36. import org.orekit.errors.TimeStampedCacheException;
  37. import org.orekit.time.AbsoluteDate;
  38. import org.orekit.time.FieldAbsoluteDate;
  39. import org.orekit.time.TimeScales;
  40. import org.orekit.time.TimeStamped;
  41. import org.orekit.time.TimeVectorFunction;
  42. import org.orekit.utils.Constants;
  43. import org.orekit.utils.GenericTimeStampedCache;
  44. import org.orekit.utils.IERSConventions;
  45. import org.orekit.utils.ImmutableTimeStampedCache;
  46. import org.orekit.utils.OrekitConfiguration;
  47. import org.orekit.utils.TimeStampedCache;
  48. import org.orekit.utils.TimeStampedGenerator;

  49. /** This class loads any kind of Earth Orientation Parameter data throughout a large time range.
  50.  * @author Pascal Parraud
  51.  * @author Evan Ward
  52.  */
  53. public class EOPHistory {

  54.     /** Default interpolation degree.
  55.      * @since 12.0
  56.      */
  57.     public static final int DEFAULT_INTERPOLATION_DEGREE = 3;

  58.     /** Interpolation degree.
  59.      * @since 12.0
  60.      */
  61.     private final int interpolationDegree;

  62.     /**
  63.      * If this history has any EOP data.
  64.      *
  65.      * @see #hasDataFor(AbsoluteDate)
  66.      */
  67.     private final boolean hasData;

  68.     /** EOP history entries. */
  69.     private final transient ImmutableTimeStampedCache<EOPEntry> cache;

  70.     /** IERS conventions to which EOP refers. */
  71.     private final IERSConventions conventions;

  72.     /** Correction to apply to EOP (may be null). */
  73.     private final transient TimeVectorFunction tidalCorrection;

  74.     /** Time scales to use when computing corrections. */
  75.     private final transient TimeScales timeScales;

  76.     /** Simple constructor.
  77.      *
  78.      * <p>This method uses the {@link DataContext#getDefault() default data context}.
  79.      *
  80.      * @param conventions IERS conventions to which EOP refers
  81.      * @param interpolationDegree interpolation degree (must be of the form 4k-1)
  82.      * @param data the EOP data to use
  83.      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
  84.      * @see #EOPHistory(IERSConventions, int, Collection, boolean, TimeScales)
  85.      */
  86.     @DefaultDataContext
  87.     protected EOPHistory(final IERSConventions conventions,
  88.                          final int interpolationDegree,
  89.                          final Collection<? extends EOPEntry> data,
  90.                          final boolean simpleEOP) {
  91.         this(conventions, interpolationDegree, data, simpleEOP, DataContext.getDefault().getTimeScales());
  92.     }

  93.     /** Simple constructor.
  94.      * @param conventions IERS conventions to which EOP refers
  95.      * @param interpolationDegree interpolation degree (must be of the form 4k-1)
  96.      * @param data the EOP data to use
  97.      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
  98.      * @param timeScales to use when computing EOP corrections.
  99.      * @since 10.1
  100.      */
  101.     public EOPHistory(final IERSConventions conventions,
  102.                       final int interpolationDegree,
  103.                       final Collection<? extends EOPEntry> data,
  104.                       final boolean simpleEOP,
  105.                       final TimeScales timeScales) {
  106.         this(conventions, interpolationDegree, data,
  107.              simpleEOP ? null : new CachedCorrection(conventions.getEOPTidalCorrection(timeScales)),
  108.              timeScales);
  109.     }

  110.     /** Simple constructor.
  111.      * @param conventions IERS conventions to which EOP refers
  112.      * @param interpolationDegree interpolation degree (must be of the form 4k-1)
  113.      * @param data the EOP data to use
  114.      * @param tidalCorrection correction to apply to EOP
  115.      * @param timeScales to use when computing EOP corrections
  116.      * @since 12
  117.      */
  118.     private EOPHistory(final IERSConventions conventions,
  119.                        final int interpolationDegree,
  120.                        final Collection<? extends EOPEntry> data,
  121.                        final TimeVectorFunction tidalCorrection,
  122.                        final TimeScales timeScales) {

  123.         // check interpolation degree is 4k-1
  124.         final int k = (interpolationDegree + 1) / 4;
  125.         if (interpolationDegree != 4 * k - 1) {
  126.             throw new OrekitException(OrekitMessages.WRONG_EOP_INTERPOLATION_DEGREE, interpolationDegree);
  127.         }

  128.         this.conventions         = conventions;
  129.         this.interpolationDegree = interpolationDegree;
  130.         this.tidalCorrection     = tidalCorrection;
  131.         this.timeScales          = timeScales;
  132.         if (data.size() >= 1) {
  133.             // enough data to interpolate
  134.             if (missSomeDerivatives(data)) {
  135.                 // we need to estimate the missing derivatives
  136.                 final ImmutableTimeStampedCache<EOPEntry> rawCache =
  137.                                 new ImmutableTimeStampedCache<>(FastMath.min(interpolationDegree + 1, data.size()), data);
  138.                 final List<EOPEntry>fixedData = new ArrayList<>();
  139.                 for (final EOPEntry entry : rawCache.getAll()) {
  140.                     fixedData.add(fixDerivatives(entry, rawCache));
  141.                 }
  142.                 cache = new ImmutableTimeStampedCache<>(FastMath.min(interpolationDegree + 1, fixedData.size()), fixedData);
  143.             } else {
  144.                 cache = new ImmutableTimeStampedCache<>(FastMath.min(interpolationDegree + 1, data.size()), data);
  145.             }
  146.             hasData = true;
  147.         } else {
  148.             // not enough data to interpolate -> always use null correction
  149.             cache   = ImmutableTimeStampedCache.emptyCache();
  150.             hasData = false;
  151.         }
  152.     }

  153.     /**
  154.      * Determine if this history uses simplified EOP corrections.
  155.      *
  156.      * @return {@code true} if tidal corrections are ignored, {@code false} otherwise.
  157.      */
  158.     public boolean isSimpleEop() {
  159.         return tidalCorrection == null;
  160.     }

  161.     /** Get interpolation degree.
  162.      * @return interpolation degree
  163.      * @since 12.0
  164.      */
  165.     public int getInterpolationDegree() {
  166.         return interpolationDegree;
  167.     }

  168.     /**
  169.      * Get the time scales used in computing EOP corrections.
  170.      *
  171.      * @return set of time scales.
  172.      * @since 10.1
  173.      */
  174.     public TimeScales getTimeScales() {
  175.         return timeScales;
  176.     }

  177.     /** Get version of the instance that does not cache tidal correction.
  178.      * @return version of the instance that does not cache tidal correction
  179.      * @since 12.0
  180.      */
  181.     public EOPHistory getEOPHistoryWithoutCachedTidalCorrection() {
  182.         return new EOPHistory(conventions, interpolationDegree, getEntries(),
  183.                               conventions.getEOPTidalCorrection(timeScales),
  184.                               timeScales);
  185.     }

  186.     /** Check if the instance caches tidal corrections.
  187.      * @return true if the instance caches tidal corrections
  188.      * @since 12.0
  189.      */
  190.     public boolean cachesTidalCorrection() {
  191.         return tidalCorrection instanceof CachedCorrection;
  192.     }

  193.     /** Get the IERS conventions to which these EOP apply.
  194.      * @return IERS conventions to which these EOP apply
  195.      */
  196.     public IERSConventions getConventions() {
  197.         return conventions;
  198.     }

  199.     /** Get the date of the first available Earth Orientation Parameters.
  200.      * @return the start date of the available data
  201.      */
  202.     public AbsoluteDate getStartDate() {
  203.         return this.cache.getEarliest().getDate();
  204.     }

  205.     /** Get the date of the last available Earth Orientation Parameters.
  206.      * @return the end date of the available data
  207.      */
  208.     public AbsoluteDate getEndDate() {
  209.         return this.cache.getLatest().getDate();
  210.     }

  211.     /** Get the UT1-UTC value.
  212.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  213.      * @param date date at which the value is desired
  214.      * @return UT1-UTC in seconds (0 if date is outside covered range)
  215.      */
  216.     public double getUT1MinusUTC(final AbsoluteDate date) {

  217.         //check if there is data for date
  218.         if (!this.hasDataFor(date)) {
  219.             // no EOP data available for this date, we use a default 0.0 offset
  220.             return (tidalCorrection == null) ? 0.0 : tidalCorrection.value(date)[2];
  221.         }

  222.         // we have EOP data -> interpolate offset
  223.         try {
  224.             final DUT1Interpolator interpolator = new DUT1Interpolator(date);
  225.             getNeighbors(date, interpolationDegree + 1).forEach(interpolator);
  226.             double interpolated = interpolator.getInterpolated();
  227.             if (tidalCorrection != null) {
  228.                 interpolated += tidalCorrection.value(date)[2];
  229.             }
  230.             return interpolated;
  231.         } catch (TimeStampedCacheException tce) {
  232.             //this should not happen because of date check above
  233.             throw new OrekitInternalError(tce);
  234.         }

  235.     }

  236.     /** Get the UT1-UTC value.
  237.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  238.      * @param date date at which the value is desired
  239.      * @param <T> type of the field elements
  240.      * @return UT1-UTC in seconds (0 if date is outside covered range)
  241.      * @since 9.0
  242.      */
  243.     public <T extends CalculusFieldElement<T>> T getUT1MinusUTC(final FieldAbsoluteDate<T> date) {

  244.         //check if there is data for date
  245.         final AbsoluteDate absDate = date.toAbsoluteDate();
  246.         if (!this.hasDataFor(absDate)) {
  247.             // no EOP data available for this date, we use a default 0.0 offset
  248.             return (tidalCorrection == null) ? date.getField().getZero() : tidalCorrection.value(date)[2];
  249.         }

  250.         // we have EOP data -> interpolate offset
  251.         try {
  252.             final FieldDUT1Interpolator<T> interpolator = new FieldDUT1Interpolator<>(date, absDate);
  253.             getNeighbors(absDate, interpolationDegree + 1).forEach(interpolator);
  254.             T interpolated = interpolator.getInterpolated();
  255.             if (tidalCorrection != null) {
  256.                 interpolated = interpolated.add(tidalCorrection.value(date)[2]);
  257.             }
  258.             return interpolated;
  259.         } catch (TimeStampedCacheException tce) {
  260.             //this should not happen because of date check above
  261.             throw new OrekitInternalError(tce);
  262.         }

  263.     }

  264.     /** Local class for DUT1 interpolation, crossing leaps safely. */
  265.     private static class DUT1Interpolator implements Consumer<EOPEntry> {

  266.         /** DUT at first entry. */
  267.         private double firstDUT;

  268.         /** Indicator for dates just before a leap occurring during the interpolation sample. */
  269.         private boolean beforeLeap;

  270.         /** Interpolator to use. */
  271.         private final HermiteInterpolator interpolator;

  272.         /** Interpolation date. */
  273.         private AbsoluteDate date;

  274.         /** Simple constructor.
  275.          * @param date interpolation date
  276.          */
  277.         DUT1Interpolator(final AbsoluteDate date) {
  278.             this.firstDUT     = Double.NaN;
  279.             this.beforeLeap   = true;
  280.             this.interpolator = new HermiteInterpolator();
  281.             this.date         = date;
  282.         }

  283.         /** {@inheritDoc} */
  284.         @Override
  285.         public void accept(final EOPEntry neighbor) {
  286.             if (Double.isNaN(firstDUT)) {
  287.                 firstDUT = neighbor.getUT1MinusUTC();
  288.             }
  289.             final double dut;
  290.             if (neighbor.getUT1MinusUTC() - firstDUT > 0.9) {
  291.                 // there was a leap second between the entries
  292.                 dut = neighbor.getUT1MinusUTC() - 1.0;
  293.                 // UTCScale considers the discontinuity to occur at the start of the leap
  294.                 // second so this code must use the same convention. EOP entries are time
  295.                 // stamped at midnight UTC so 1 second before is the start of the leap
  296.                 // second.
  297.                 if (neighbor.getDate().shiftedBy(-1).compareTo(date) <= 0) {
  298.                     beforeLeap = false;
  299.                 }
  300.             } else {
  301.                 dut = neighbor.getUT1MinusUTC();
  302.             }
  303.             interpolator.addSamplePoint(neighbor.getDate().durationFrom(date),
  304.                                         new double[] {
  305.                                             dut
  306.                                         });
  307.         }

  308.         /** Get the interpolated value.
  309.          * @return interpolated value
  310.          */
  311.         public double getInterpolated() {
  312.             final double interpolated = interpolator.value(0)[0];
  313.             return beforeLeap ? interpolated : interpolated + 1.0;
  314.         }

  315.     }

  316.     /** Local class for DUT1 interpolation, crossing leaps safely. */
  317.     private static class FieldDUT1Interpolator<T extends CalculusFieldElement<T>> implements Consumer<EOPEntry> {

  318.         /** DUT at first entry. */
  319.         private double firstDUT;

  320.         /** Indicator for dates just before a leap occurring during the interpolation sample. */
  321.         private boolean beforeLeap;

  322.         /** Interpolator to use. */
  323.         private final FieldHermiteInterpolator<T> interpolator;

  324.         /** Interpolation date. */
  325.         private FieldAbsoluteDate<T> date;

  326.         /** Interpolation date. */
  327.         private AbsoluteDate absDate;

  328.         /** Simple constructor.
  329.          * @param date interpolation date
  330.          * @param absDate interpolation date
  331.          */
  332.         FieldDUT1Interpolator(final FieldAbsoluteDate<T> date, final AbsoluteDate absDate) {
  333.             this.firstDUT     = Double.NaN;
  334.             this.beforeLeap   = true;
  335.             this.interpolator = new FieldHermiteInterpolator<>();
  336.             this.date         = date;
  337.             this.absDate      = absDate;
  338.         }

  339.         /** {@inheritDoc} */
  340.         @Override
  341.         public void accept(final EOPEntry neighbor) {
  342.             if (Double.isNaN(firstDUT)) {
  343.                 firstDUT = neighbor.getUT1MinusUTC();
  344.             }
  345.             final double dut;
  346.             if (neighbor.getUT1MinusUTC() - firstDUT > 0.9) {
  347.                 // there was a leap second between the entries
  348.                 dut = neighbor.getUT1MinusUTC() - 1.0;
  349.                 if (neighbor.getDate().compareTo(absDate) <= 0) {
  350.                     beforeLeap = false;
  351.                 }
  352.             } else {
  353.                 dut = neighbor.getUT1MinusUTC();
  354.             }
  355.             final T[] array = MathArrays.buildArray(date.getField(), 1);
  356.             array[0] = date.getField().getZero().newInstance(dut);
  357.             interpolator.addSamplePoint(date.durationFrom(neighbor.getDate()).negate(),
  358.                                         array);
  359.         }

  360.         /** Get the interpolated value.
  361.          * @return interpolated value
  362.          */
  363.         public T getInterpolated() {
  364.             final T interpolated = interpolator.value(date.getField().getZero())[0];
  365.             return beforeLeap ? interpolated : interpolated.add(1.0);
  366.         }

  367.     }

  368.     /**
  369.      * Get the entries surrounding a central date.
  370.      * <p>
  371.      * See {@link #hasDataFor(AbsoluteDate)} to determine if the cache has data
  372.      * for {@code central} without throwing an exception.
  373.      *
  374.      * @param central central date
  375.      * @param n number of neighbors
  376.      * @return array of cached entries surrounding specified date
  377.      * @since 12.0
  378.      */
  379.     protected Stream<EOPEntry> getNeighbors(final AbsoluteDate central, final int n) {
  380.         return cache.getNeighbors(central, n);
  381.     }

  382.     /** Get the LoD (Length of Day) value.
  383.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  384.      * @param date date at which the value is desired
  385.      * @return LoD in seconds (0 if date is outside covered range)
  386.      */
  387.     public double getLOD(final AbsoluteDate date) {

  388.         // check if there is data for date
  389.         if (!this.hasDataFor(date)) {
  390.             // no EOP data available for this date, we use a default null correction
  391.             return (tidalCorrection == null) ? 0.0 : tidalCorrection.value(date)[3];
  392.         }

  393.         // we have EOP data for date -> interpolate correction
  394.         double interpolated = interpolate(date, EOPEntry::getLOD);
  395.         if (tidalCorrection != null) {
  396.             interpolated += tidalCorrection.value(date)[3];
  397.         }
  398.         return interpolated;

  399.     }

  400.     /** Get the LoD (Length of Day) value.
  401.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  402.      * @param date date at which the value is desired
  403.      * @param <T> type of the filed elements
  404.      * @return LoD in seconds (0 if date is outside covered range)
  405.      * @since 9.0
  406.      */
  407.     public <T extends CalculusFieldElement<T>> T getLOD(final FieldAbsoluteDate<T> date) {

  408.         final AbsoluteDate aDate = date.toAbsoluteDate();

  409.         // check if there is data for date
  410.         if (!this.hasDataFor(aDate)) {
  411.             // no EOP data available for this date, we use a default null correction
  412.             return (tidalCorrection == null) ? date.getField().getZero() : tidalCorrection.value(date)[3];
  413.         }

  414.         // we have EOP data for date -> interpolate correction
  415.         T interpolated = interpolate(date, aDate, EOPEntry::getLOD);
  416.         if (tidalCorrection != null) {
  417.             interpolated = interpolated.add(tidalCorrection.value(date)[3]);
  418.         }

  419.         return interpolated;

  420.     }

  421.     /** Get the pole IERS Reference Pole correction.
  422.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  423.      * @param date date at which the correction is desired
  424.      * @return pole correction ({@link PoleCorrection#NULL_CORRECTION
  425.      * PoleCorrection.NULL_CORRECTION} if date is outside covered range)
  426.      */
  427.     public PoleCorrection getPoleCorrection(final AbsoluteDate date) {

  428.         // check if there is data for date
  429.         if (!this.hasDataFor(date)) {
  430.             // no EOP data available for this date, we use a default null correction
  431.             if (tidalCorrection == null) {
  432.                 return PoleCorrection.NULL_CORRECTION;
  433.             } else {
  434.                 final double[] correction = tidalCorrection.value(date);
  435.                 return new PoleCorrection(correction[0], correction[1]);
  436.             }
  437.         }

  438.         // we have EOP data for date -> interpolate correction
  439.         final double[] interpolated = interpolate(date,
  440.                 EOPEntry::getX, EOPEntry::getY,
  441.                 EOPEntry::getXRate, EOPEntry::getYRate);
  442.         if (tidalCorrection != null) {
  443.             final double[] correction = tidalCorrection.value(date);
  444.             interpolated[0] += correction[0];
  445.             interpolated[1] += correction[1];
  446.         }
  447.         return new PoleCorrection(interpolated[0], interpolated[1]);

  448.     }

  449.     /** Get the pole IERS Reference Pole correction.
  450.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  451.      * @param date date at which the correction is desired
  452.      * @param <T> type of the field elements
  453.      * @return pole correction ({@link PoleCorrection#NULL_CORRECTION
  454.      * PoleCorrection.NULL_CORRECTION} if date is outside covered range)
  455.      */
  456.     public <T extends CalculusFieldElement<T>> FieldPoleCorrection<T> getPoleCorrection(final FieldAbsoluteDate<T> date) {

  457.         final AbsoluteDate aDate = date.toAbsoluteDate();

  458.         // check if there is data for date
  459.         if (!this.hasDataFor(aDate)) {
  460.             // no EOP data available for this date, we use a default null correction
  461.             if (tidalCorrection == null) {
  462.                 return new FieldPoleCorrection<>(date.getField().getZero(), date.getField().getZero());
  463.             } else {
  464.                 final T[] correction = tidalCorrection.value(date);
  465.                 return new FieldPoleCorrection<>(correction[0], correction[1]);
  466.             }
  467.         }

  468.         // we have EOP data for date -> interpolate correction
  469.         final T[] interpolated = interpolate(date, aDate, EOPEntry::getX, EOPEntry::getY);
  470.         if (tidalCorrection != null) {
  471.             final T[] correction = tidalCorrection.value(date);
  472.             interpolated[0] = interpolated[0].add(correction[0]);
  473.             interpolated[1] = interpolated[1].add(correction[1]);
  474.         }
  475.         return new FieldPoleCorrection<>(interpolated[0], interpolated[1]);

  476.     }

  477.     /** Get the correction to the nutation parameters for equinox-based paradigm.
  478.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  479.      * @param date date at which the correction is desired
  480.      * @return nutation correction in longitude ΔΨ and in obliquity Δε
  481.      * (zero if date is outside covered range)
  482.      */
  483.     public double[] getEquinoxNutationCorrection(final AbsoluteDate date) {

  484.         // check if there is data for date
  485.         if (!this.hasDataFor(date)) {
  486.             // no EOP data available for this date, we use a default null correction
  487.             return new double[2];
  488.         }

  489.         // we have EOP data for date -> interpolate correction
  490.         return interpolate(date, EOPEntry::getDdPsi, EOPEntry::getDdEps);

  491.     }

  492.     /** Get the correction to the nutation parameters for equinox-based paradigm.
  493.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  494.      * @param date date at which the correction is desired
  495.      * @param <T> type of the field elements
  496.      * @return nutation correction in longitude ΔΨ and in obliquity Δε
  497.      * (zero if date is outside covered range)
  498.      */
  499.     public <T extends CalculusFieldElement<T>> T[] getEquinoxNutationCorrection(final FieldAbsoluteDate<T> date) {

  500.         final AbsoluteDate aDate = date.toAbsoluteDate();

  501.         // check if there is data for date
  502.         if (!this.hasDataFor(aDate)) {
  503.             // no EOP data available for this date, we use a default null correction
  504.             return MathArrays.buildArray(date.getField(), 2);
  505.         }

  506.         // we have EOP data for date -> interpolate correction
  507.         return interpolate(date, aDate, EOPEntry::getDdPsi, EOPEntry::getDdEps);

  508.     }

  509.     /** Get the correction to the nutation parameters for Non-Rotating Origin paradigm.
  510.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  511.      * @param date date at which the correction is desired
  512.      * @return nutation correction in Celestial Intermediate Pole coordinates
  513.      * δX and δY (zero if date is outside covered range)
  514.      */
  515.     public double[] getNonRotatinOriginNutationCorrection(final AbsoluteDate date) {

  516.         // check if there is data for date
  517.         if (!this.hasDataFor(date)) {
  518.             // no EOP data available for this date, we use a default null correction
  519.             return new double[2];
  520.         }

  521.         // we have EOP data for date -> interpolate correction
  522.         return interpolate(date, EOPEntry::getDx, EOPEntry::getDy);

  523.     }

  524.     /** Get the correction to the nutation parameters for Non-Rotating Origin paradigm.
  525.      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
  526.      * @param date date at which the correction is desired
  527.      * @param <T> type of the filed elements
  528.      * @return nutation correction in Celestial Intermediate Pole coordinates
  529.      * δX and δY (zero if date is outside covered range)
  530.      */
  531.     public <T extends CalculusFieldElement<T>> T[] getNonRotatinOriginNutationCorrection(final FieldAbsoluteDate<T> date) {

  532.         final AbsoluteDate aDate = date.toAbsoluteDate();

  533.         // check if there is data for date
  534.         if (!this.hasDataFor(aDate)) {
  535.             // no EOP data available for this date, we use a default null correction
  536.             return MathArrays.buildArray(date.getField(), 2);
  537.         }

  538.         // we have EOP data for date -> interpolate correction
  539.         return interpolate(date, aDate, EOPEntry::getDx, EOPEntry::getDy);

  540.     }

  541.     /** Get the ITRF version.
  542.      * @param date date at which the value is desired
  543.      * @return ITRF version of the EOP covering the specified date
  544.      * @since 9.2
  545.      */
  546.     public ITRFVersion getITRFVersion(final AbsoluteDate date) {

  547.         // check if there is data for date
  548.         if (!this.hasDataFor(date)) {
  549.             // no EOP data available for this date, we use a default ITRF 2014
  550.             return ITRFVersion.ITRF_2014;
  551.         }

  552.         try {
  553.             // we have EOP data for date
  554.             final Optional<EOPEntry> first = getNeighbors(date, 1).findFirst();
  555.             return first.isPresent() ? first.get().getITRFType() : ITRFVersion.ITRF_2014;

  556.         } catch (TimeStampedCacheException tce) {
  557.             // this should not happen because of date check performed at start
  558.             throw new OrekitInternalError(tce);
  559.         }

  560.     }

  561.     /** Check Earth orientation parameters continuity.
  562.      * @param maxGap maximal allowed gap between entries (in seconds)
  563.      */
  564.     public void checkEOPContinuity(final double maxGap) {
  565.         TimeStamped preceding = null;
  566.         for (final TimeStamped current : this.cache.getAll()) {

  567.             // compare the dates of preceding and current entries
  568.             if (preceding != null && (current.getDate().durationFrom(preceding.getDate())) > maxGap) {
  569.                 throw new OrekitException(OrekitMessages.MISSING_EARTH_ORIENTATION_PARAMETERS_BETWEEN_DATES_GAP,
  570.                                           preceding.getDate(), current.getDate(),
  571.                                           current.getDate().durationFrom(preceding.getDate()));
  572.             }

  573.             // prepare next iteration
  574.             preceding = current;

  575.         }
  576.     }

  577.     /**
  578.      * Check if the cache has data for the given date using
  579.      * {@link #getStartDate()} and {@link #getEndDate()}.
  580.      *
  581.      * @param date the requested date
  582.      * @return true if the {@link #cache} has data for the requested date, false
  583.      *         otherwise.
  584.      */
  585.     protected boolean hasDataFor(final AbsoluteDate date) {
  586.         /*
  587.          * when there is no EOP data, short circuit getStartDate, which will
  588.          * throw an exception
  589.          */
  590.         return this.hasData && this.getStartDate().compareTo(date) <= 0 &&
  591.                date.compareTo(this.getEndDate()) <= 0;
  592.     }

  593.     /** Get a non-modifiable view of the EOP entries.
  594.      * @return non-modifiable view of the EOP entries
  595.      */
  596.     public List<EOPEntry> getEntries() {
  597.         return cache.getAll();
  598.     }

  599.     /** Interpolate a single EOP component.
  600.      * <p>
  601.      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
  602.      * </p>
  603.      * @param date interpolation date
  604.      * @param selector selector for EOP entry component
  605.      * @return interpolated value
  606.      */
  607.     private double interpolate(final AbsoluteDate date, final Function<EOPEntry, Double> selector) {
  608.         try {
  609.             final HermiteInterpolator interpolator = new HermiteInterpolator();
  610.             getNeighbors(date, interpolationDegree + 1).
  611.                 forEach(entry -> interpolator.addSamplePoint(entry.getDate().durationFrom(date),
  612.                                                              new double[] {
  613.                                                                  selector.apply(entry)
  614.                                                              }));
  615.             return interpolator.value(0)[0];
  616.         } catch (TimeStampedCacheException tce) {
  617.             // this should not happen because of date check performed by caller
  618.             throw new OrekitInternalError(tce);
  619.         }
  620.     }

  621.     /** Interpolate a single EOP component.
  622.      * <p>
  623.      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
  624.      * </p>
  625.      * @param date interpolation date
  626.      * @param aDate interpolation date, as an {@link AbsoluteDate}
  627.      * @param selector selector for EOP entry component
  628.      * @param <T> type of the field elements
  629.      * @return interpolated value
  630.      */
  631.     private <T extends CalculusFieldElement<T>> T interpolate(final FieldAbsoluteDate<T> date,
  632.                                                               final AbsoluteDate aDate,
  633.                                                               final Function<EOPEntry, Double> selector) {
  634.         try {
  635.             final FieldHermiteInterpolator<T> interpolator = new FieldHermiteInterpolator<>();
  636.             final T[] y = MathArrays.buildArray(date.getField(), 1);
  637.             final T zero = date.getField().getZero();
  638.             final FieldAbsoluteDate<T> central = new FieldAbsoluteDate<>(aDate, zero); // here, we attempt to get a constant date,
  639.                                                                                        // for example removing derivatives
  640.                                                                                        // if T was DerivativeStructure
  641.             getNeighbors(aDate, interpolationDegree + 1).
  642.                 forEach(entry -> {
  643.                     y[0] = zero.newInstance(selector.apply(entry));
  644.                     interpolator.addSamplePoint(central.durationFrom(entry.getDate()).negate(), y);
  645.                 });
  646.             return interpolator.value(date.durationFrom(central))[0]; // here, we introduce derivatives again (in DerivativeStructure case)
  647.         } catch (TimeStampedCacheException tce) {
  648.             // this should not happen because of date check performed by caller
  649.             throw new OrekitInternalError(tce);
  650.         }
  651.     }

  652.     /** Interpolate two EOP components.
  653.      * <p>
  654.      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
  655.      * </p>
  656.      * @param date interpolation date
  657.      * @param selector1 selector for first EOP entry component
  658.      * @param selector2 selector for second EOP entry component
  659.      * @return interpolated value
  660.      */
  661.     private double[] interpolate(final AbsoluteDate date,
  662.                                  final Function<EOPEntry, Double> selector1,
  663.                                  final Function<EOPEntry, Double> selector2) {
  664.         try {
  665.             final HermiteInterpolator interpolator = new HermiteInterpolator();
  666.             getNeighbors(date, interpolationDegree + 1).
  667.                 forEach(entry -> interpolator.addSamplePoint(entry.getDate().durationFrom(date),
  668.                                                              new double[] {
  669.                                                                  selector1.apply(entry),
  670.                                                                  selector2.apply(entry)
  671.                                                              }));
  672.             return interpolator.value(0);
  673.         } catch (TimeStampedCacheException tce) {
  674.             // this should not happen because of date check performed by caller
  675.             throw new OrekitInternalError(tce);
  676.         }
  677.     }

  678.     /** Interpolate two EOP components.
  679.      * <p>
  680.      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
  681.      * </p>
  682.      * @param date interpolation date
  683.      * @param selector1 selector for first EOP entry component
  684.      * @param selector2 selector for second EOP entry component
  685.      * @param selector1Rate selector for first EOP entry component rate
  686.      * @param selector2Rate selector for second EOP entry component rate
  687.      * @return interpolated value
  688.      * @since 12.0
  689.      */
  690.     private double[] interpolate(final AbsoluteDate date,
  691.                                  final Function<EOPEntry, Double> selector1,
  692.                                  final Function<EOPEntry, Double> selector2,
  693.                                  final Function<EOPEntry, Double> selector1Rate,
  694.                                  final Function<EOPEntry, Double> selector2Rate) {
  695.         try {
  696.             final HermiteInterpolator interpolator = new HermiteInterpolator();
  697.             getNeighbors(date, (interpolationDegree + 1) / 2).
  698.                 forEach(entry -> interpolator.addSamplePoint(entry.getDate().durationFrom(date),
  699.                                                              new double[] {
  700.                                                                  selector1.apply(entry),
  701.                                                                  selector2.apply(entry)
  702.                                                              },
  703.                                                              new double[] {
  704.                                                                  selector1Rate.apply(entry),
  705.                                                                  selector2Rate.apply(entry)
  706.                                                              }));
  707.             return interpolator.value(0);
  708.         } catch (TimeStampedCacheException tce) {
  709.             // this should not happen because of date check performed by caller
  710.             throw new OrekitInternalError(tce);
  711.         }
  712.     }

  713.     /** Interpolate two EOP components.
  714.      * <p>
  715.      * This method should be called <em>only</em> when {@link #hasDataFor(AbsoluteDate)} returns true.
  716.      * </p>
  717.      * @param date interpolation date
  718.      * @param aDate interpolation date, as an {@link AbsoluteDate}
  719.      * @param selector1 selector for first EOP entry component
  720.      * @param selector2 selector for second EOP entry component
  721.      * @param <T> type of the field elements
  722.      * @return interpolated value
  723.      */
  724.     private <T extends CalculusFieldElement<T>> T[] interpolate(final FieldAbsoluteDate<T> date,
  725.                                                                 final AbsoluteDate aDate,
  726.                                                                 final Function<EOPEntry, Double> selector1,
  727.                                                                 final Function<EOPEntry, Double> selector2) {
  728.         try {
  729.             final FieldHermiteInterpolator<T> interpolator = new FieldHermiteInterpolator<>();
  730.             final T[] y = MathArrays.buildArray(date.getField(), 2);
  731.             final T zero = date.getField().getZero();
  732.             final FieldAbsoluteDate<T> central = new FieldAbsoluteDate<>(aDate, zero); // here, we attempt to get a constant date,
  733.                                                                                        // for example removing derivatives
  734.                                                                                        // if T was DerivativeStructure
  735.             getNeighbors(aDate, interpolationDegree + 1).
  736.                 forEach(entry -> {
  737.                     y[0] = zero.newInstance(selector1.apply(entry));
  738.                     y[1] = zero.newInstance(selector2.apply(entry));
  739.                     interpolator.addSamplePoint(central.durationFrom(entry.getDate()).negate(), y);
  740.                 });
  741.             return interpolator.value(date.durationFrom(central)); // here, we introduce derivatives again (in DerivativeStructure case)
  742.         } catch (TimeStampedCacheException tce) {
  743.             // this should not happen because of date check performed by caller
  744.             throw new OrekitInternalError(tce);
  745.         }
  746.     }

  747.     /** Check if some derivatives are missing.
  748.      * @param raw raw EOP history
  749.      * @return true if some derivatives are missing
  750.      * @since 12.0
  751.      */
  752.     private boolean missSomeDerivatives(final Collection<? extends EOPEntry> raw) {
  753.         for (final EOPEntry entry : raw) {
  754.             if (Double.isNaN(entry.getLOD() + entry.getXRate() + entry.getYRate())) {
  755.                 return true;
  756.             }
  757.         }
  758.         return false;
  759.     }

  760.     /** Fix missing derivatives.
  761.      * @param entry EOP entry to fix
  762.      * @param rawCache raw EOP history cache
  763.      * @return fixed entry
  764.      * @since 12.0
  765.      */
  766.     private EOPEntry fixDerivatives(final EOPEntry entry, final ImmutableTimeStampedCache<EOPEntry> rawCache) {

  767.         // helper function to differentiate some EOP parameters
  768.         final BiFunction<EOPEntry, Function<EOPEntry, Double>, Double> differentiator =
  769.                         (e, selector) -> {
  770.                             final HermiteInterpolator interpolator = new HermiteInterpolator();
  771.                             rawCache.getNeighbors(e.getDate()).
  772.                                 forEach(f -> interpolator.addSamplePoint(f.getDate().durationFrom(e.getDate()),
  773.                                                                          new double[] {
  774.                                                                              selector.apply(f)
  775.                                                                          }));
  776.                             return interpolator.derivatives(0.0, 1)[1][0];
  777.                         };

  778.         if (Double.isNaN(entry.getLOD() + entry.getXRate() + entry.getYRate())) {
  779.             final double lod   = Double.isNaN(entry.getLOD()) ?
  780.                                  -differentiator.apply(entry, EOPEntry::getUT1MinusUTC) :
  781.                                  entry.getLOD();
  782.             final double xRate = Double.isNaN(entry.getXRate()) ?
  783.                                  differentiator.apply(entry, EOPEntry::getX) :
  784.                                  entry.getXRate();
  785.             final double yRate = Double.isNaN(entry.getYRate()) ?
  786.                                  differentiator.apply(entry, EOPEntry::getY) :
  787.                                  entry.getYRate();
  788.             return new EOPEntry(entry.getMjd(),
  789.                                 entry.getUT1MinusUTC(), lod,
  790.                                 entry.getX(), entry.getY(), xRate, yRate,
  791.                                 entry.getDdPsi(), entry.getDdEps(),
  792.                                 entry.getDx(), entry.getDy(),
  793.                                 entry.getITRFType(), entry.getDate());
  794.         } else {
  795.             // the entry already has all derivatives
  796.             return entry;
  797.         }
  798.     }

  799.     /** Internal class for caching tidal correction. */
  800.     private static class TidalCorrectionEntry implements TimeStamped {

  801.         /** Entry date. */
  802.         private final AbsoluteDate date;

  803.         /** Correction. */
  804.         private final double[] correction;

  805.         /** Simple constructor.
  806.          * @param date entry date
  807.          * @param correction correction on the EOP parameters (xp, yp, ut1, lod)
  808.          */
  809.         TidalCorrectionEntry(final AbsoluteDate date, final double[] correction) {
  810.             this.date       = date;
  811.             this.correction = correction;
  812.         }

  813.         /** {@inheritDoc} */
  814.         @Override
  815.         public AbsoluteDate getDate() {
  816.             return date;
  817.         }

  818.     }

  819.     /** Local generator for thread-safe cache. */
  820.     private static class CachedCorrection
  821.         implements TimeVectorFunction, TimeStampedGenerator<TidalCorrectionEntry> {

  822.         /** Correction to apply to EOP (may be null). */
  823.         private final TimeVectorFunction tidalCorrection;

  824.         /** Step between generated entries. */
  825.         private final double step;

  826.         /** Tidal corrections entries cache. */
  827.         private final TimeStampedCache<TidalCorrectionEntry> cache;

  828.         /** Simple constructor.
  829.          * @param tidalCorrection function computing the tidal correction
  830.          */
  831.         CachedCorrection(final TimeVectorFunction tidalCorrection) {
  832.             this.step            = 60 * 60;
  833.             this.tidalCorrection = tidalCorrection;
  834.             this.cache           =
  835.                     new GenericTimeStampedCache<>(8,
  836.                             OrekitConfiguration.getCacheSlotsNumber(),
  837.                             Constants.JULIAN_DAY * 30,
  838.                             Constants.JULIAN_DAY,
  839.                             this);
  840.         }

  841.         /** {@inheritDoc} */
  842.         @Override
  843.         public double[] value(final AbsoluteDate date) {
  844.             try {
  845.                 // set up an interpolator
  846.                 final HermiteInterpolator interpolator = new HermiteInterpolator();
  847.                 cache.getNeighbors(date).forEach(entry -> interpolator.addSamplePoint(entry.date.durationFrom(date), entry.correction));

  848.                 // interpolate to specified date
  849.                 return interpolator.value(0.0);
  850.             } catch (TimeStampedCacheException tsce) {
  851.                 // this should never happen
  852.                 throw new OrekitInternalError(tsce);
  853.             }
  854.         }

  855.         /** {@inheritDoc} */
  856.         @Override
  857.         public <T extends CalculusFieldElement<T>> T[] value(final FieldAbsoluteDate<T> date) {
  858.             try {

  859.                 final AbsoluteDate aDate = date.toAbsoluteDate();

  860.                 final FieldHermiteInterpolator<T> interpolator = new FieldHermiteInterpolator<>();
  861.                 final T[] y = MathArrays.buildArray(date.getField(), 4);
  862.                 final T zero = date.getField().getZero();
  863.                 final FieldAbsoluteDate<T> central = new FieldAbsoluteDate<>(aDate, zero); // here, we attempt to get a constant date,
  864.                                                                                            // for example removing derivatives
  865.                                                                                            // if T was DerivativeStructure
  866.                 cache.getNeighbors(aDate).forEach(entry -> {
  867.                     for (int i = 0; i < y.length; ++i) {
  868.                         y[i] = zero.newInstance(entry.correction[i]);
  869.                     }
  870.                     interpolator.addSamplePoint(central.durationFrom(entry.getDate()).negate(), y);
  871.                 });

  872.                 // interpolate to specified date
  873.                 return interpolator.value(date.durationFrom(central)); // here, we introduce derivatives again (in DerivativeStructure case)

  874.             } catch (TimeStampedCacheException tsce) {
  875.                 // this should never happen
  876.                 throw new OrekitInternalError(tsce);
  877.             }
  878.         }

  879.         /** {@inheritDoc} */
  880.         @Override
  881.         public List<TidalCorrectionEntry> generate(final AbsoluteDate existingDate, final AbsoluteDate date) {

  882.             final List<TidalCorrectionEntry> generated = new ArrayList<>();

  883.             if (existingDate == null) {

  884.                 // no prior existing entries, just generate a first set
  885.                 for (int i = -cache.getMaxNeighborsSize() / 2; generated.size() < cache.getMaxNeighborsSize(); ++i) {
  886.                     final AbsoluteDate t = date.shiftedBy(i * step);
  887.                     generated.add(new TidalCorrectionEntry(t, tidalCorrection.value(t)));
  888.                 }

  889.             } else {

  890.                 // some entries have already been generated
  891.                 // add the missing ones up to specified date

  892.                 AbsoluteDate t = existingDate;
  893.                 if (date.compareTo(t) > 0) {
  894.                     // forward generation
  895.                     do {
  896.                         t = t.shiftedBy(step);
  897.                         generated.add(new TidalCorrectionEntry(t, tidalCorrection.value(t)));
  898.                     } while (t.compareTo(date) <= 0);
  899.                 } else {
  900.                     // backward generation
  901.                     do {
  902.                         t = t.shiftedBy(-step);
  903.                         generated.add(0, new TidalCorrectionEntry(t, tidalCorrection.value(t)));
  904.                     } while (t.compareTo(date) >= 0);
  905.                 }
  906.             }

  907.             // return the generated transforms
  908.             return generated;

  909.         }
  910.     }

  911. }