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