1   /* Copyright 2013-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.rugged.utils;
18  
19  import java.io.Serializable;
20  import java.util.ArrayList;
21  import java.util.List;
22  import java.util.stream.Collectors;
23  
24  import org.hipparchus.geometry.euclidean.threed.Rotation;
25  import org.hipparchus.geometry.euclidean.threed.Vector3D;
26  import org.hipparchus.util.FastMath;
27  import org.orekit.frames.FactoryManagedFrame;
28  import org.orekit.frames.Frame;
29  import org.orekit.frames.FramesFactory;
30  import org.orekit.frames.Predefined;
31  import org.orekit.frames.Transform;
32  import org.orekit.rugged.errors.DumpManager;
33  import org.orekit.rugged.errors.RuggedException;
34  import org.orekit.rugged.errors.RuggedMessages;
35  import org.orekit.time.AbsoluteDate;
36  import org.orekit.time.TimeInterpolator;
37  import org.orekit.time.TimeOffset;
38  import org.orekit.utils.AngularCoordinates;
39  import org.orekit.utils.AngularDerivativesFilter;
40  import org.orekit.utils.CartesianDerivativesFilter;
41  import org.orekit.utils.ImmutableTimeStampedCache;
42  import org.orekit.utils.PVCoordinates;
43  import org.orekit.utils.TimeStampedAngularCoordinates;
44  import org.orekit.utils.TimeStampedAngularCoordinatesHermiteInterpolator;
45  import org.orekit.utils.TimeStampedCache;
46  import org.orekit.utils.TimeStampedPVCoordinates;
47  import org.orekit.utils.TimeStampedPVCoordinatesHermiteInterpolator;
48  
49  /** Provider for observation transforms.
50   * @author Luc Maisonobe
51   * @author Guylaine Prat
52   */
53  public class SpacecraftToObservedBody implements Serializable {
54  
55      /** Serializable UID. */
56      private static final long serialVersionUID = 20250427L;
57  
58      /** Inertial frame. */
59      private final Frame inertialFrame;
60  
61      /** Body frame. */
62      private final Frame bodyFrame;
63  
64      /** Start of search time span. */
65      private final AbsoluteDate minDate;
66  
67      /** End of search time span. */
68      private final AbsoluteDate maxDate;
69  
70      /** Step to use for inertial frame to body frame transforms cache computations. */
71      private final double tStep;
72  
73      /** Tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting. */
74      private final double overshootTolerance;
75  
76      /** Transforms sample from observed body frame to inertial frame. */
77      private final List<Transform> bodyToInertial;
78  
79      /** Transforms sample from inertial frame to observed body frame. */
80      private final List<Transform> inertialToBody;
81  
82      /** Transforms sample from spacecraft frame to inertial frame. */
83      private final List<Transform> scToInertial;
84  
85      /** Simple constructor.
86       * @param inertialFrame inertial frame
87       * @param bodyFrame observed body frame
88       * @param minDate start of search time span
89       * @param maxDate end of search time span
90       * @param tStep step to use for inertial frame to body frame transforms cache computations
91       * @param overshootTolerance tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting
92       * slightly the position, velocity and quaternions ephemerides
93       * @param positionsVelocities satellite position and velocity
94       * @param pvInterpolationNumber number of points to use for position/velocity interpolation
95       * @param pvFilter filter for derivatives from the sample to use in position/velocity interpolation
96       * @param quaternions satellite quaternions
97       * @param aInterpolationNumber number of points to use for attitude interpolation
98       * @param aFilter filter for derivatives from the sample to use in attitude interpolation
99       */
100     public SpacecraftToObservedBody(final Frame inertialFrame, final Frame bodyFrame,
101                                     final AbsoluteDate minDate, final AbsoluteDate maxDate, final double tStep,
102                                     final double overshootTolerance,
103                                     final List<TimeStampedPVCoordinates> positionsVelocities, final int pvInterpolationNumber,
104                                     final CartesianDerivativesFilter pvFilter,
105                                     final List<TimeStampedAngularCoordinates> quaternions, final int aInterpolationNumber,
106                                     final AngularDerivativesFilter aFilter) {
107 
108         this.inertialFrame      = inertialFrame;
109         this.bodyFrame          = bodyFrame;
110         this.minDate            = minDate;
111         this.maxDate            = maxDate;
112         this.overshootTolerance = overshootTolerance;
113 
114         // safety checks
115         final AbsoluteDate minPVDate = positionsVelocities.get(0).getDate();
116         final AbsoluteDate maxPVDate = positionsVelocities.get(positionsVelocities.size() - 1).getDate();
117         if (minPVDate.durationFrom(minDate) > overshootTolerance) {
118             throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, minDate, minPVDate, maxPVDate);
119         }
120         if (maxDate.durationFrom(maxPVDate) > overshootTolerance) {
121             throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, maxDate, minPVDate, maxPVDate);
122         }
123 
124         final AbsoluteDate minQDate  = quaternions.get(0).getDate();
125         final AbsoluteDate maxQDate  = quaternions.get(quaternions.size() - 1).getDate();
126         if (minQDate.durationFrom(minDate) > overshootTolerance) {
127             throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, minDate, minQDate, maxQDate);
128         }
129         if (maxDate.durationFrom(maxQDate) > overshootTolerance) {
130             throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, maxDate, minQDate, maxQDate);
131         }
132 
133         // set up the cache for position-velocities
134         final TimeStampedCache<TimeStampedPVCoordinates> pvCache =
135                 new ImmutableTimeStampedCache<>(pvInterpolationNumber, positionsVelocities);
136 
137         // set up the TimeStampedPVCoordinates interpolator
138         final TimeInterpolator<TimeStampedPVCoordinates> pvInterpolator =
139                 new TimeStampedPVCoordinatesHermiteInterpolator(pvInterpolationNumber, pvFilter);
140 
141         // set up the cache for attitudes
142         final TimeStampedCache<TimeStampedAngularCoordinates> aCache =
143                 new ImmutableTimeStampedCache<>(aInterpolationNumber, quaternions);
144 
145         // set up the TimeStampedAngularCoordinates Hermite interpolator
146         final TimeInterpolator<TimeStampedAngularCoordinates> angularInterpolator =
147                 new TimeStampedAngularCoordinatesHermiteInterpolator(aInterpolationNumber, aFilter);
148 
149         final int n = (int) FastMath.ceil(maxDate.durationFrom(minDate) / tStep);
150         this.tStep          = tStep;
151         this.bodyToInertial = new ArrayList<>(n);
152         this.inertialToBody = new ArrayList<>(n);
153         this.scToInertial   = new ArrayList<>(n);
154         for (AbsoluteDate date = minDate; bodyToInertial.size() < n; date = date.shiftedBy(tStep)) {
155 
156             // interpolate position-velocity, allowing slight extrapolation near the boundaries
157             final AbsoluteDate pvInterpolationDate;
158             if (date.compareTo(pvCache.getEarliest().getDate()) < 0) {
159                 pvInterpolationDate = pvCache.getEarliest().getDate();
160             } else if (date.compareTo(pvCache.getLatest().getDate()) > 0) {
161                 pvInterpolationDate = pvCache.getLatest().getDate();
162             } else {
163                 pvInterpolationDate = date;
164             }
165             final TimeStampedPVCoordinates interpolatedPV =
166                     pvInterpolator.interpolate(pvInterpolationDate,
167                             pvCache.getNeighbors(pvInterpolationDate));
168             final TimeStampedPVCoordinates pv = interpolatedPV.shiftedBy(date.durationFrom(pvInterpolationDate));
169 
170             // interpolate attitude, allowing slight extrapolation near the boundaries
171             final AbsoluteDate aInterpolationDate;
172             if (date.compareTo(aCache.getEarliest().getDate()) < 0) {
173                 aInterpolationDate = aCache.getEarliest().getDate();
174             } else if (date.compareTo(aCache.getLatest().getDate()) > 0) {
175                 aInterpolationDate = aCache.getLatest().getDate();
176             } else {
177                 aInterpolationDate = date;
178             }
179             final TimeStampedAngularCoordinates interpolatedQuaternion =
180                     angularInterpolator.interpolate(aInterpolationDate,
181                             aCache.getNeighbors(aInterpolationDate).collect(Collectors.toList()));
182             final TimeStampedAngularCoordinates quaternion = interpolatedQuaternion.shiftedBy(date.durationFrom(aInterpolationDate));
183 
184             // store transform from spacecraft frame to inertial frame
185             scToInertial.add(new Transform(date,
186                     new Transform(date, quaternion.revert()),
187                     new Transform(date, pv)));
188 
189             // store transform from body frame to inertial frame
190             final Transform b2i = bodyFrame.getTransformTo(inertialFrame, date);
191             bodyToInertial.add(b2i);
192             inertialToBody.add(b2i.getInverse());
193 
194         }
195     }
196 
197     /** Simple constructor.
198      * @param inertialFrame inertial frame
199      * @param bodyFrame observed body frame
200      * @param minDate start of search time span
201      * @param maxDate end of search time span
202      * @param tStep step to use for inertial frame to body frame transforms cache computations
203      * @param overshootTolerance tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting
204      * slightly the position, velocity and quaternions ephemerides
205      * @param bodyToInertial transforms sample from observed body frame to inertial frame
206      * @param scToInertial transforms sample from spacecraft frame to inertial frame
207      */
208     public SpacecraftToObservedBody(final Frame inertialFrame, final Frame bodyFrame,
209                                     final AbsoluteDate minDate, final AbsoluteDate maxDate, final double tStep,
210                                     final double overshootTolerance,
211                                     final List<Transform> bodyToInertial, final List<Transform> scToInertial) {
212 
213         this.inertialFrame      = inertialFrame;
214         this.bodyFrame          = bodyFrame;
215         this.minDate            = minDate;
216         this.maxDate            = maxDate;
217         this.tStep              = tStep;
218         this.overshootTolerance = overshootTolerance;
219         this.bodyToInertial     = bodyToInertial;
220         this.scToInertial       = scToInertial;
221 
222         this.inertialToBody = new ArrayList<>(bodyToInertial.size());
223         for (final Transform b2i : bodyToInertial) {
224             inertialToBody.add(b2i.getInverse());
225         }
226 
227     }
228 
229     /** Get the inertial frame.
230      * @return inertial frame
231      */
232     public Frame getInertialFrame() {
233         return inertialFrame;
234     }
235 
236     /** Get the body frame.
237      * @return body frame
238      */
239     public Frame getBodyFrame() {
240         return bodyFrame;
241     }
242 
243     /** Get the start of search time span.
244      * @return start of search time span
245      */
246     public AbsoluteDate getMinDate() {
247         return minDate;
248     }
249 
250     /** Get the end of search time span.
251      * @return end of search time span
252      */
253     public AbsoluteDate getMaxDate() {
254         return maxDate;
255     }
256 
257     /** Get the step to use for inertial frame to body frame transforms cache computations.
258      * @return step to use for inertial frame to body frame transforms cache computations
259      */
260     public double getTStep() {
261         return tStep;
262     }
263 
264     /** Get the tolerance in seconds allowed for {@link #getMinDate()} and {@link #getMaxDate()} overshooting.
265      * @return tolerance in seconds allowed for {@link #getMinDate()} and {@link #getMaxDate()} overshooting
266      */
267     public double getOvershootTolerance() {
268         return overshootTolerance;
269     }
270 
271     /** Get transform from spacecraft to inertial frame.
272      * @param date date of the transform
273      * @return transform from spacecraft to inertial frame
274      */
275     public Transform getScToInertial(final AbsoluteDate date) {
276         return interpolate(date, scToInertial);
277     }
278 
279     /** Get transform from inertial frame to observed body frame.
280      * @param date date of the transform
281      * @return transform from inertial frame to observed body frame
282      */
283     public Transform getInertialToBody(final AbsoluteDate date) {
284         return interpolate(date, inertialToBody);
285     }
286 
287     /** Get transform from observed body frame to inertial frame.
288      * @param date date of the transform
289      * @return transform from observed body frame to inertial frame
290      */
291     public Transform getBodyToInertial(final AbsoluteDate date) {
292         return interpolate(date, bodyToInertial);
293     }
294 
295     /** Interpolate transform.
296      * @param date date of the transform
297      * @param list transforms list to interpolate from
298      * @return interpolated transform
299      */
300     private Transform interpolate(final AbsoluteDate date, final List<Transform> list) {
301 
302         // check date range
303         if (!isInRange(date)) {
304             throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, date, minDate, maxDate);
305         }
306 
307         final double    s     = date.durationFrom(list.get(0).getDate()) / tStep;
308         final int       index = FastMath.max(0, FastMath.min(list.size() - 1, (int) FastMath.rint(s)));
309 
310         DumpManager.dumpTransform(this, index, bodyToInertial.get(index), scToInertial.get(index));
311 
312         final Transform close = list.get(index);
313         return close.shiftedBy(date.durationFrom(close.getDate()));
314 
315     }
316 
317     /** Check if a date is in the supported range.
318      * @param date date to check
319      * @return true if date is in the supported range
320      */
321     public boolean isInRange(final AbsoluteDate date) {
322         return minDate.durationFrom(date) <= overshootTolerance &&
323                date.durationFrom(maxDate) <= overshootTolerance;
324     }
325 
326     /** Replace the instance with a data transfer object for serialization.
327      * @return data transfer object that will be serialized
328      */
329     private Object writeReplace() {
330         return new DataTransferObject(((FactoryManagedFrame) inertialFrame).getFactoryKey(),
331                                       ((FactoryManagedFrame) bodyFrame).getFactoryKey(),
332                                       minDate, maxDate, tStep, overshootTolerance,
333                                       extractTimeOffsets(bodyToInertial),
334                                       extractCoordinates(bodyToInertial),
335                                       extractTimeOffsets(scToInertial),
336                                       extractCoordinates(scToInertial));
337     }
338 
339     /** Extract time offsets from a transforms list.
340      * @param transforms transforms to convert
341      * @return time offsets
342      * @since 4.0
343      */
344     private long[] extractTimeOffsets(final List<Transform> transforms) {
345 
346         final long[] offsets = new long[2 * transforms.size()];
347 
348         for (int i = 0; i < transforms.size(); ++i) {
349             final Transform ti = transforms.get(i);
350             offsets[2 * i]     = ti.getDate().getSeconds();
351             offsets[2 * i + 1] = ti.getDate().getAttoSeconds();
352         }
353 
354         return offsets;
355 
356     }
357 
358     /** Extract coordinates from a transforms list.
359      * @param transforms transforms to convert
360      * @return time offsets
361      * @since 4.0
362      */
363     private double[] extractCoordinates(final List<Transform> transforms) {
364 
365         final double[] coordinates = new double[19 * transforms.size()];
366 
367         for (int i = 0; i < transforms.size(); ++i) {
368 
369             final PVCoordinates      pv = transforms.get(i).getCartesian();
370             final AngularCoordinates ag = transforms.get(i).getAngular();
371 
372             coordinates[19 * i]      = pv.getPosition().getX();
373             coordinates[19 * i +  1] = pv.getPosition().getY();
374             coordinates[19 * i +  2] = pv.getPosition().getZ();
375 
376             coordinates[19 * i +  3] = pv.getVelocity().getX();
377             coordinates[19 * i +  4] = pv.getVelocity().getY();
378             coordinates[19 * i +  5] = pv.getVelocity().getZ();
379 
380             coordinates[19 * i +  6] = pv.getAcceleration().getX();
381             coordinates[19 * i +  7] = pv.getAcceleration().getY();
382             coordinates[19 * i +  8] = pv.getAcceleration().getZ();
383 
384             coordinates[19 * i +  9] = ag.getRotation().getQ0();
385             coordinates[19 * i + 10] = ag.getRotation().getQ1();
386             coordinates[19 * i + 11] = ag.getRotation().getQ2();
387             coordinates[19 * i + 12] = ag.getRotation().getQ3();
388 
389             coordinates[19 * i + 13] = ag.getRotationRate().getX();
390             coordinates[19 * i + 14] = ag.getRotationRate().getY();
391             coordinates[19 * i + 15] = ag.getRotationRate().getZ();
392 
393             coordinates[19 * i + 16] = ag.getRotationAcceleration().getX();
394             coordinates[19 * i + 17] = ag.getRotationAcceleration().getY();
395             coordinates[19 * i + 18] = ag.getRotationAcceleration().getZ();
396 
397         }
398 
399         return coordinates;
400 
401     }
402 
403     /** Internal class used only for serialization.
404      * @since 4.0
405      */
406     private static class DataTransferObject implements Serializable {
407 
408         /** Serializable UID. */
409         private static final long serialVersionUID = 20250427L;
410 
411         /** Inertial frame. */
412         private final Predefined inertialFrame;
413 
414         /** Body frame. */
415         private final Predefined bodyFrame;
416 
417         /** Start of search time span. */
418         private final AbsoluteDate minDate;
419 
420         /** End of search time span. */
421         private final AbsoluteDate maxDate;
422 
423         /** Step to use for inertial frame to body frame transforms cache computations. */
424         private final double tStep;
425 
426         /** Tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting. */
427         private final double overshootTolerance;
428 
429         /** Transforms sample from observed body frame to inertial frame. */
430         private final long[] bodyToInertialTimeOffset;
431 
432         /** Transforms sample from observed body frame to inertial frame. */
433         private final double[] bodyToInertialCoordinates;
434 
435         /** Transforms sample from spacecraft frame to inertial frame. */
436         private final long[] scToInertialTimOffset;
437 
438         /** Transforms sample from spacecraft frame to inertial frame. */
439         private final double[] scToInertialCoordinates;
440 
441         /** Simple constructor.
442          * @param inertialFrame inertial frame
443          * @param bodyFrame observed body frame
444          * @param minDate start of search time span
445          * @param maxDate end of search time span
446          * @param tStep step to use for inertial frame to body frame transforms cache computations
447          * @param overshootTolerance tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting
448          * slightly the position, velocity and quaternions ephemerides
449          * @param bodyToInertialTimeOffset time offsets of transforms sample from observed body frame to inertial frame
450          * @param bodyToInertialCoordinates coordinates of transforms sample from observed body frame to inertial frame
451          * @param scToInertialTimOffset time offsets transforms sample from spacecraft frame to inertial frame
452          * @param scToInertialCoordinates coordinates transforms sample from spacecraft frame to inertial frame
453          */
454         DataTransferObject(final Predefined inertialFrame, final Predefined bodyFrame,
455                            final AbsoluteDate minDate, final AbsoluteDate maxDate, final double tStep,
456                            final double overshootTolerance,
457                            final long[] bodyToInertialTimeOffset, final double[] bodyToInertialCoordinates,
458                            final long[] scToInertialTimOffset, final double[] scToInertialCoordinates) {
459             this.inertialFrame             = inertialFrame;
460             this.bodyFrame                 = bodyFrame;
461             this.minDate                   = minDate;
462             this.maxDate                   = maxDate;
463             this.tStep                     = tStep;
464             this.overshootTolerance        = overshootTolerance;
465             this.bodyToInertialTimeOffset  = bodyToInertialTimeOffset;
466             this.bodyToInertialCoordinates = bodyToInertialCoordinates;
467             this.scToInertialTimOffset     = scToInertialTimOffset;
468             this.scToInertialCoordinates   = scToInertialCoordinates;
469         }
470 
471         /** Create a transform.
472          * @param i index of the transfor
473          * @param timeOffsets time offsets array
474          * @param coordinates coordinates array
475          * @return transform
476          */
477         private Transform createTransform(final int i,
478                                           final long[] timeOffsets,
479                                           final double[] coordinates) {
480             final AbsoluteDate date = new AbsoluteDate(new TimeOffset(timeOffsets[2 * i],
481                                                                       timeOffsets[2 * i + 1]));
482             final PVCoordinates pv = new PVCoordinates(new Vector3D(coordinates[19 * i],
483                                                                     coordinates[19 * i +  1],
484                                                                     coordinates[19 * i +  2]),
485                                                        new Vector3D(coordinates[19 * i +  3],
486                                                                     coordinates[19 * i +  4],
487                                                                     coordinates[19 * i +  5]),
488                                                        new Vector3D(coordinates[19 * i +  6],
489                                                                     coordinates[19 * i +  7],
490                                                                     coordinates[19 * i +  8]));
491             final AngularCoordinates ag = new AngularCoordinates(new Rotation(coordinates[19 * i +  9],
492                                                                               coordinates[19 * i + 10],
493                                                                               coordinates[19 * i + 11],
494                                                                               coordinates[19 * i + 12],
495                                                                               false),
496                                                                  new Vector3D(coordinates[19 * i + 13],
497                                                                               coordinates[19 * i + 14],
498                                                                               coordinates[19 * i + 15]),
499                                                                  new Vector3D(coordinates[19 * i + 16],
500                                                                               coordinates[19 * i + 17],
501                                                                               coordinates[19 * i + 18]));
502             return new Transform(date, pv, ag);
503         }
504 
505         /** Create all transforms.
506          * @param timeOffsets time offsets array
507          * @param coordinates coordinates array
508          * @return all transforms
509          */
510         private List<Transform> createAllTransforms(final long[] timeOffsets,
511                                                     final double[] coordinates) {
512             final List<Transform> transforms = new ArrayList<>(timeOffsets.length / 2);
513             for (int i = 0; i < timeOffsets.length / 2; ++i) {
514                 transforms.add(createTransform(i, timeOffsets, coordinates));
515             }
516             return transforms;
517         }
518 
519         /** Replace the deserialized data transfer object with a
520          * {@link SpacecraftToObservedBody}.
521          * @return replacement {@link SpacecraftToObservedBody}
522          */
523         private Object readResolve() {
524             return new SpacecraftToObservedBody(FramesFactory.getFrame(inertialFrame),
525                                                 FramesFactory.getFrame(bodyFrame),
526                                                 minDate, maxDate, tStep, overshootTolerance,
527                                                 createAllTransforms(bodyToInertialTimeOffset,
528                                                                     bodyToInertialCoordinates),
529                                                 createAllTransforms(scToInertialTimOffset,
530                                                                     scToInertialCoordinates));
531         }
532 
533     }
534 
535 }