EOPHistory.java

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

  18. import java.io.Serializable;
  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. import org.hipparchus.CalculusFieldElement;
  28. import org.hipparchus.analysis.interpolation.FieldHermiteInterpolator;
  29. import org.hipparchus.analysis.interpolation.HermiteInterpolator;
  30. import org.hipparchus.util.FastMath;
  31. import org.hipparchus.util.MathArrays;
  32. import org.orekit.annotation.DefaultDataContext;
  33. import org.orekit.data.DataContext;
  34. import org.orekit.errors.OrekitException;
  35. import org.orekit.errors.OrekitInternalError;
  36. import org.orekit.errors.OrekitMessages;
  37. import org.orekit.errors.TimeStampedCacheException;
  38. import org.orekit.time.AbsoluteDate;
  39. import org.orekit.time.FieldAbsoluteDate;
  40. import org.orekit.time.TimeScales;
  41. import org.orekit.time.TimeStamped;
  42. import org.orekit.time.TimeVectorFunction;
  43. import org.orekit.utils.Constants;
  44. import org.orekit.utils.GenericTimeStampedCache;
  45. import org.orekit.utils.IERSConventions;
  46. import org.orekit.utils.ImmutableTimeStampedCache;
  47. import org.orekit.utils.OrekitConfiguration;
  48. import org.orekit.utils.TimeStampedCache;
  49. import org.orekit.utils.TimeStampedGenerator;

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

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

  59.     /** Serializable UID. */
  60.     private static final long serialVersionUID = 20231022L;

  61.     /** Interpolation degree.
  62.      * @since 12.0
  63.      */
  64.     private final int interpolationDegree;

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

  71.     /** EOP history entries. */
  72.     private final transient ImmutableTimeStampedCache<EOPEntry> cache;

  73.     /** IERS conventions to which EOP refers. */
  74.     private final IERSConventions conventions;

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

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

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

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

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

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

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

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

  164.     /** Get interpolation degree.
  165.      * @return interpolation degree
  166.      * @since 12.0
  167.      */
  168.     public int getInterpolationDegree() {
  169.         return interpolationDegree;
  170.     }

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

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

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

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

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

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

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

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

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

  238.     }

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

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

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

  266.     }

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

  269.         /** DUT at first entry. */
  270.         private double firstDUT;

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

  273.         /** Interpolator to use. */
  274.         private final HermiteInterpolator interpolator;

  275.         /** Interpolation date. */
  276.         private AbsoluteDate date;

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

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

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

  318.     }

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

  321.         /** DUT at first entry. */
  322.         private double firstDUT;

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

  325.         /** Interpolator to use. */
  326.         private final FieldHermiteInterpolator<T> interpolator;

  327.         /** Interpolation date. */
  328.         private FieldAbsoluteDate<T> date;

  329.         /** Interpolation date. */
  330.         private AbsoluteDate absDate;

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

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

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

  370.     }

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

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

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

  396.         // we have EOP data for date -> interpolate correction
  397.         double interpolated = interpolate(date, entry -> entry.getLOD());
  398.         if (tidalCorrection != null) {
  399.             interpolated += tidalCorrection.value(date)[3];
  400.         }
  401.         return interpolated;

  402.     }

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

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

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

  417.         // we have EOP data for date -> interpolate correction
  418.         T interpolated = interpolate(date, aDate, entry -> entry.getLOD());
  419.         if (tidalCorrection != null) {
  420.             interpolated = interpolated.add(tidalCorrection.value(date)[3]);
  421.         }

  422.         return interpolated;

  423.     }

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

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

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

  451.     }

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

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

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

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

  479.     }

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

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

  492.         // we have EOP data for date -> interpolate correction
  493.         return interpolate(date, entry -> entry.getDdPsi(), entry -> entry.getDdEps());

  494.     }

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

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

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

  509.         // we have EOP data for date -> interpolate correction
  510.         return interpolate(date, aDate, entry -> entry.getDdPsi(), entry -> entry.getDdEps());

  511.     }

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

  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 new double[2];
  523.         }

  524.         // we have EOP data for date -> interpolate correction
  525.         return interpolate(date, entry -> entry.getDx(), entry -> entry.getDy());

  526.     }

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

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

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

  541.         // we have EOP data for date -> interpolate correction
  542.         return interpolate(date, aDate, entry -> entry.getDx(), entry -> entry.getDy());

  543.     }

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

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

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

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

  563.     }

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

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

  576.             // prepare next iteration
  577.             preceding = current;

  578.         }
  579.     }

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

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

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

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

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

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

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

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

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

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

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

  802.     /** Replace the instance with a data transfer object for serialization.
  803.      * @return data transfer object that will be serialized
  804.      */
  805.     @DefaultDataContext
  806.     private Object writeReplace() {
  807.         return new DataTransferObject(conventions, interpolationDegree, getEntries(), tidalCorrection == null);
  808.     }

  809.     /** Internal class used only for serialization. */
  810.     @DefaultDataContext
  811.     private static class DataTransferObject implements Serializable {

  812.         /** Serializable UID. */
  813.         private static final long serialVersionUID = 20231122L;

  814.         /** IERS conventions. */
  815.         private final IERSConventions conventions;

  816.         /** Interpolation degree.
  817.          * @since 12.0
  818.          */
  819.         private final int interpolationDegree;

  820.         /** EOP entries. */
  821.         private final List<EOPEntry> entries;

  822.         /** Indicator for simple interpolation without tidal effects. */
  823.         private final boolean simpleEOP;

  824.         /** Simple constructor.
  825.          * @param conventions IERS conventions to which EOP refers
  826.          * @param interpolationDegree interpolation degree (must be of the form 4k-1)
  827.          * @param entries the EOP data to use
  828.          * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
  829.          * @since 12.0
  830.          */
  831.         DataTransferObject(final IERSConventions conventions,
  832.                            final int interpolationDegree,
  833.                            final List<EOPEntry> entries,
  834.                            final boolean simpleEOP) {
  835.             this.conventions         = conventions;
  836.             this.interpolationDegree = interpolationDegree;
  837.             this.entries             = entries;
  838.             this.simpleEOP           = simpleEOP;
  839.         }

  840.         /** Replace the deserialized data transfer object with a {@link EOPHistory}.
  841.          * @return replacement {@link EOPHistory}
  842.          */
  843.         private Object readResolve() {
  844.             try {
  845.                 return new EOPHistory(conventions, interpolationDegree, entries, simpleEOP);
  846.             } catch (OrekitException oe) {
  847.                 throw new OrekitInternalError(oe);
  848.             }
  849.         }

  850.     }

  851.     /** Internal class for caching tidal correction. */
  852.     private static class TidalCorrectionEntry implements TimeStamped {

  853.         /** Entry date. */
  854.         private final AbsoluteDate date;

  855.         /** Correction. */
  856.         private final double[] correction;

  857.         /** Simple constructor.
  858.          * @param date entry date
  859.          * @param correction correction on the EOP parameters (xp, yp, ut1, lod)
  860.          */
  861.         TidalCorrectionEntry(final AbsoluteDate date, final double[] correction) {
  862.             this.date       = date;
  863.             this.correction = correction;
  864.         }

  865.         /** {@inheritDoc} */
  866.         @Override
  867.         public AbsoluteDate getDate() {
  868.             return date;
  869.         }

  870.     }

  871.     /** Local generator for thread-safe cache. */
  872.     private static class CachedCorrection
  873.         implements TimeVectorFunction, TimeStampedGenerator<TidalCorrectionEntry> {

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

  876.         /** Step between generated entries. */
  877.         private final double step;

  878.         /** Tidal corrections entries cache. */
  879.         private final TimeStampedCache<TidalCorrectionEntry> cache;

  880.         /** Simple constructor.
  881.          * @param tidalCorrection function computing the tidal correction
  882.          */
  883.         CachedCorrection(final TimeVectorFunction tidalCorrection) {
  884.             this.step            = 60 * 60;
  885.             this.tidalCorrection = tidalCorrection;
  886.             this.cache           =
  887.                 new GenericTimeStampedCache<TidalCorrectionEntry>(8,
  888.                                                                   OrekitConfiguration.getCacheSlotsNumber(),
  889.                                                                   Constants.JULIAN_DAY * 30,
  890.                                                                   Constants.JULIAN_DAY,
  891.                                                                   this);
  892.         }

  893.         /** {@inheritDoc} */
  894.         @Override
  895.         public double[] value(final AbsoluteDate date) {
  896.             try {
  897.                 // set up an interpolator
  898.                 final HermiteInterpolator interpolator = new HermiteInterpolator();
  899.                 cache.getNeighbors(date).forEach(entry -> interpolator.addSamplePoint(entry.date.durationFrom(date), entry.correction));

  900.                 // interpolate to specified date
  901.                 return interpolator.value(0.0);
  902.             } catch (TimeStampedCacheException tsce) {
  903.                 // this should never happen
  904.                 throw new OrekitInternalError(tsce);
  905.             }
  906.         }

  907.         /** {@inheritDoc} */
  908.         @Override
  909.         public <T extends CalculusFieldElement<T>> T[] value(final FieldAbsoluteDate<T> date) {
  910.             try {

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

  912.                 final FieldHermiteInterpolator<T> interpolator = new FieldHermiteInterpolator<>();
  913.                 final T[] y = MathArrays.buildArray(date.getField(), 4);
  914.                 final T zero = date.getField().getZero();
  915.                 final FieldAbsoluteDate<T> central = new FieldAbsoluteDate<>(aDate, zero); // here, we attempt to get a constant date,
  916.                                                                                            // for example removing derivatives
  917.                                                                                            // if T was DerivativeStructure
  918.                 cache.getNeighbors(aDate).forEach(entry -> {
  919.                     for (int i = 0; i < y.length; ++i) {
  920.                         y[i] = zero.newInstance(entry.correction[i]);
  921.                     }
  922.                     interpolator.addSamplePoint(central.durationFrom(entry.getDate()).negate(), y);
  923.                 });

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

  926.             } catch (TimeStampedCacheException tsce) {
  927.                 // this should never happen
  928.                 throw new OrekitInternalError(tsce);
  929.             }
  930.         }

  931.         /** {@inheritDoc} */
  932.         @Override
  933.         public List<TidalCorrectionEntry> generate(final AbsoluteDate existingDate, final AbsoluteDate date) {

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

  935.             if (existingDate == null) {

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

  941.             } else {

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

  944.                 AbsoluteDate t = existingDate;
  945.                 if (date.compareTo(t) > 0) {
  946.                     // forward generation
  947.                     do {
  948.                         t = t.shiftedBy(step);
  949.                         generated.add(new TidalCorrectionEntry(t, tidalCorrection.value(t)));
  950.                     } while (t.compareTo(date) <= 0);
  951.                 } else {
  952.                     // backward generation
  953.                     do {
  954.                         t = t.shiftedBy(-step);
  955.                         generated.add(0, new TidalCorrectionEntry(t, tidalCorrection.value(t)));
  956.                     } while (t.compareTo(date) >= 0);
  957.                 }
  958.             }

  959.             // return the generated transforms
  960.             return generated;

  961.         }
  962.     }

  963. }