SpacecraftToObservedBody.java

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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.hipparchus.geometry.euclidean.threed.Rotation;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.orekit.frames.FactoryManagedFrame;
import org.orekit.frames.Frame;
import org.orekit.frames.FramesFactory;
import org.orekit.frames.Predefined;
import org.orekit.frames.Transform;
import org.orekit.rugged.errors.DumpManager;
import org.orekit.rugged.errors.RuggedException;
import org.orekit.rugged.errors.RuggedMessages;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeInterpolator;
import org.orekit.time.TimeOffset;
import org.orekit.utils.AngularCoordinates;
import org.orekit.utils.AngularDerivativesFilter;
import org.orekit.utils.CartesianDerivativesFilter;
import org.orekit.utils.ImmutableTimeStampedCache;
import org.orekit.utils.PVCoordinates;
import org.orekit.utils.TimeStampedAngularCoordinates;
import org.orekit.utils.TimeStampedAngularCoordinatesHermiteInterpolator;
import org.orekit.utils.TimeStampedCache;
import org.orekit.utils.TimeStampedPVCoordinates;
import org.orekit.utils.TimeStampedPVCoordinatesHermiteInterpolator;

/** Provider for observation transforms.
 * @author Luc Maisonobe
 * @author Guylaine Prat
 */
public class SpacecraftToObservedBody implements Serializable {

    /** Serializable UID. */
    private static final long serialVersionUID = 20250427L;

    /** Inertial frame. */
    private final Frame inertialFrame;

    /** Body frame. */
    private final Frame bodyFrame;

    /** Start of search time span. */
    private final AbsoluteDate minDate;

    /** End of search time span. */
    private final AbsoluteDate maxDate;

    /** Step to use for inertial frame to body frame transforms cache computations. */
    private final double tStep;

    /** Tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting. */
    private final double overshootTolerance;

    /** Transforms sample from observed body frame to inertial frame. */
    private final List<Transform> bodyToInertial;

    /** Transforms sample from inertial frame to observed body frame. */
    private final List<Transform> inertialToBody;

    /** Transforms sample from spacecraft frame to inertial frame. */
    private final List<Transform> scToInertial;

    /** Simple constructor.
     * @param inertialFrame inertial frame
     * @param bodyFrame observed body frame
     * @param minDate start of search time span
     * @param maxDate end of search time span
     * @param tStep step to use for inertial frame to body frame transforms cache computations
     * @param overshootTolerance tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting
     * slightly the position, velocity and quaternions ephemerides
     * @param positionsVelocities satellite position and velocity
     * @param pvInterpolationNumber number of points to use for position/velocity interpolation
     * @param pvFilter filter for derivatives from the sample to use in position/velocity interpolation
     * @param quaternions satellite quaternions
     * @param aInterpolationNumber number of points to use for attitude interpolation
     * @param aFilter filter for derivatives from the sample to use in attitude interpolation
     */
    public SpacecraftToObservedBody(final Frame inertialFrame, final Frame bodyFrame,
                                    final AbsoluteDate minDate, final AbsoluteDate maxDate, final double tStep,
                                    final double overshootTolerance,
                                    final List<TimeStampedPVCoordinates> positionsVelocities, final int pvInterpolationNumber,
                                    final CartesianDerivativesFilter pvFilter,
                                    final List<TimeStampedAngularCoordinates> quaternions, final int aInterpolationNumber,
                                    final AngularDerivativesFilter aFilter) {

        this.inertialFrame      = inertialFrame;
        this.bodyFrame          = bodyFrame;
        this.minDate            = minDate;
        this.maxDate            = maxDate;
        this.overshootTolerance = overshootTolerance;

        // safety checks
        final AbsoluteDate minPVDate = positionsVelocities.get(0).getDate();
        final AbsoluteDate maxPVDate = positionsVelocities.get(positionsVelocities.size() - 1).getDate();
        if (minPVDate.durationFrom(minDate) > overshootTolerance) {
            throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, minDate, minPVDate, maxPVDate);
        }
        if (maxDate.durationFrom(maxPVDate) > overshootTolerance) {
            throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, maxDate, minPVDate, maxPVDate);
        }

        final AbsoluteDate minQDate  = quaternions.get(0).getDate();
        final AbsoluteDate maxQDate  = quaternions.get(quaternions.size() - 1).getDate();
        if (minQDate.durationFrom(minDate) > overshootTolerance) {
            throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, minDate, minQDate, maxQDate);
        }
        if (maxDate.durationFrom(maxQDate) > overshootTolerance) {
            throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, maxDate, minQDate, maxQDate);
        }

        // set up the cache for position-velocities
        final TimeStampedCache<TimeStampedPVCoordinates> pvCache =
                new ImmutableTimeStampedCache<>(pvInterpolationNumber, positionsVelocities);

        // set up the TimeStampedPVCoordinates interpolator
        final TimeInterpolator<TimeStampedPVCoordinates> pvInterpolator =
                new TimeStampedPVCoordinatesHermiteInterpolator(pvInterpolationNumber, pvFilter);

        // set up the cache for attitudes
        final TimeStampedCache<TimeStampedAngularCoordinates> aCache =
                new ImmutableTimeStampedCache<>(aInterpolationNumber, quaternions);

        // set up the TimeStampedAngularCoordinates Hermite interpolator
        final TimeInterpolator<TimeStampedAngularCoordinates> angularInterpolator =
                new TimeStampedAngularCoordinatesHermiteInterpolator(aInterpolationNumber, aFilter);

        final int n = (int) FastMath.ceil(maxDate.durationFrom(minDate) / tStep);
        this.tStep          = tStep;
        this.bodyToInertial = new ArrayList<>(n);
        this.inertialToBody = new ArrayList<>(n);
        this.scToInertial   = new ArrayList<>(n);
        for (AbsoluteDate date = minDate; bodyToInertial.size() < n; date = date.shiftedBy(tStep)) {

            // interpolate position-velocity, allowing slight extrapolation near the boundaries
            final AbsoluteDate pvInterpolationDate;
            if (date.compareTo(pvCache.getEarliest().getDate()) < 0) {
                pvInterpolationDate = pvCache.getEarliest().getDate();
            } else if (date.compareTo(pvCache.getLatest().getDate()) > 0) {
                pvInterpolationDate = pvCache.getLatest().getDate();
            } else {
                pvInterpolationDate = date;
            }
            final TimeStampedPVCoordinates interpolatedPV =
                    pvInterpolator.interpolate(pvInterpolationDate,
                            pvCache.getNeighbors(pvInterpolationDate));
            final TimeStampedPVCoordinates pv = interpolatedPV.shiftedBy(date.durationFrom(pvInterpolationDate));

            // interpolate attitude, allowing slight extrapolation near the boundaries
            final AbsoluteDate aInterpolationDate;
            if (date.compareTo(aCache.getEarliest().getDate()) < 0) {
                aInterpolationDate = aCache.getEarliest().getDate();
            } else if (date.compareTo(aCache.getLatest().getDate()) > 0) {
                aInterpolationDate = aCache.getLatest().getDate();
            } else {
                aInterpolationDate = date;
            }
            final TimeStampedAngularCoordinates interpolatedQuaternion =
                    angularInterpolator.interpolate(aInterpolationDate,
                            aCache.getNeighbors(aInterpolationDate).collect(Collectors.toList()));
            final TimeStampedAngularCoordinates quaternion = interpolatedQuaternion.shiftedBy(date.durationFrom(aInterpolationDate));

            // store transform from spacecraft frame to inertial frame
            scToInertial.add(new Transform(date,
                    new Transform(date, quaternion.revert()),
                    new Transform(date, pv)));

            // store transform from body frame to inertial frame
            final Transform b2i = bodyFrame.getTransformTo(inertialFrame, date);
            bodyToInertial.add(b2i);
            inertialToBody.add(b2i.getInverse());

        }
    }

    /** Simple constructor.
     * @param inertialFrame inertial frame
     * @param bodyFrame observed body frame
     * @param minDate start of search time span
     * @param maxDate end of search time span
     * @param tStep step to use for inertial frame to body frame transforms cache computations
     * @param overshootTolerance tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting
     * slightly the position, velocity and quaternions ephemerides
     * @param bodyToInertial transforms sample from observed body frame to inertial frame
     * @param scToInertial transforms sample from spacecraft frame to inertial frame
     */
    public SpacecraftToObservedBody(final Frame inertialFrame, final Frame bodyFrame,
                                    final AbsoluteDate minDate, final AbsoluteDate maxDate, final double tStep,
                                    final double overshootTolerance,
                                    final List<Transform> bodyToInertial, final List<Transform> scToInertial) {

        this.inertialFrame      = inertialFrame;
        this.bodyFrame          = bodyFrame;
        this.minDate            = minDate;
        this.maxDate            = maxDate;
        this.tStep              = tStep;
        this.overshootTolerance = overshootTolerance;
        this.bodyToInertial     = bodyToInertial;
        this.scToInertial       = scToInertial;

        this.inertialToBody = new ArrayList<>(bodyToInertial.size());
        for (final Transform b2i : bodyToInertial) {
            inertialToBody.add(b2i.getInverse());
        }

    }

    /** Get the inertial frame.
     * @return inertial frame
     */
    public Frame getInertialFrame() {
        return inertialFrame;
    }

    /** Get the body frame.
     * @return body frame
     */
    public Frame getBodyFrame() {
        return bodyFrame;
    }

    /** Get the start of search time span.
     * @return start of search time span
     */
    public AbsoluteDate getMinDate() {
        return minDate;
    }

    /** Get the end of search time span.
     * @return end of search time span
     */
    public AbsoluteDate getMaxDate() {
        return maxDate;
    }

    /** Get the step to use for inertial frame to body frame transforms cache computations.
     * @return step to use for inertial frame to body frame transforms cache computations
     */
    public double getTStep() {
        return tStep;
    }

    /** Get the tolerance in seconds allowed for {@link #getMinDate()} and {@link #getMaxDate()} overshooting.
     * @return tolerance in seconds allowed for {@link #getMinDate()} and {@link #getMaxDate()} overshooting
     */
    public double getOvershootTolerance() {
        return overshootTolerance;
    }

    /** Get transform from spacecraft to inertial frame.
     * @param date date of the transform
     * @return transform from spacecraft to inertial frame
     */
    public Transform getScToInertial(final AbsoluteDate date) {
        return interpolate(date, scToInertial);
    }

    /** Get transform from inertial frame to observed body frame.
     * @param date date of the transform
     * @return transform from inertial frame to observed body frame
     */
    public Transform getInertialToBody(final AbsoluteDate date) {
        return interpolate(date, inertialToBody);
    }

    /** Get transform from observed body frame to inertial frame.
     * @param date date of the transform
     * @return transform from observed body frame to inertial frame
     */
    public Transform getBodyToInertial(final AbsoluteDate date) {
        return interpolate(date, bodyToInertial);
    }

    /** Interpolate transform.
     * @param date date of the transform
     * @param list transforms list to interpolate from
     * @return interpolated transform
     */
    private Transform interpolate(final AbsoluteDate date, final List<Transform> list) {

        // check date range
        if (!isInRange(date)) {
            throw new RuggedException(RuggedMessages.OUT_OF_TIME_RANGE, date, minDate, maxDate);
        }

        final double    s     = date.durationFrom(list.get(0).getDate()) / tStep;
        final int       index = FastMath.max(0, FastMath.min(list.size() - 1, (int) FastMath.rint(s)));

        DumpManager.dumpTransform(this, index, bodyToInertial.get(index), scToInertial.get(index));

        final Transform close = list.get(index);
        return close.shiftedBy(date.durationFrom(close.getDate()));

    }

    /** Check if a date is in the supported range.
     * @param date date to check
     * @return true if date is in the supported range
     */
    public boolean isInRange(final AbsoluteDate date) {
        return minDate.durationFrom(date) <= overshootTolerance &&
               date.durationFrom(maxDate) <= overshootTolerance;
    }

    /** Replace the instance with a data transfer object for serialization.
     * @return data transfer object that will be serialized
     */
    private Object writeReplace() {
        return new DataTransferObject(((FactoryManagedFrame) inertialFrame).getFactoryKey(),
                                      ((FactoryManagedFrame) bodyFrame).getFactoryKey(),
                                      minDate, maxDate, tStep, overshootTolerance,
                                      extractTimeOffsets(bodyToInertial),
                                      extractCoordinates(bodyToInertial),
                                      extractTimeOffsets(scToInertial),
                                      extractCoordinates(scToInertial));
    }

    /** Extract time offsets from a transforms list.
     * @param transforms transforms to convert
     * @return time offsets
     * @since 4.0
     */
    private long[] extractTimeOffsets(final List<Transform> transforms) {

        final long[] offsets = new long[2 * transforms.size()];

        for (int i = 0; i < transforms.size(); ++i) {
            final Transform ti = transforms.get(i);
            offsets[2 * i]     = ti.getDate().getSeconds();
            offsets[2 * i + 1] = ti.getDate().getAttoSeconds();
        }

        return offsets;

    }

    /** Extract coordinates from a transforms list.
     * @param transforms transforms to convert
     * @return time offsets
     * @since 4.0
     */
    private double[] extractCoordinates(final List<Transform> transforms) {

        final double[] coordinates = new double[19 * transforms.size()];

        for (int i = 0; i < transforms.size(); ++i) {

            final PVCoordinates      pv = transforms.get(i).getCartesian();
            final AngularCoordinates ag = transforms.get(i).getAngular();

            coordinates[19 * i]      = pv.getPosition().getX();
            coordinates[19 * i +  1] = pv.getPosition().getY();
            coordinates[19 * i +  2] = pv.getPosition().getZ();

            coordinates[19 * i +  3] = pv.getVelocity().getX();
            coordinates[19 * i +  4] = pv.getVelocity().getY();
            coordinates[19 * i +  5] = pv.getVelocity().getZ();

            coordinates[19 * i +  6] = pv.getAcceleration().getX();
            coordinates[19 * i +  7] = pv.getAcceleration().getY();
            coordinates[19 * i +  8] = pv.getAcceleration().getZ();

            coordinates[19 * i +  9] = ag.getRotation().getQ0();
            coordinates[19 * i + 10] = ag.getRotation().getQ1();
            coordinates[19 * i + 11] = ag.getRotation().getQ2();
            coordinates[19 * i + 12] = ag.getRotation().getQ3();

            coordinates[19 * i + 13] = ag.getRotationRate().getX();
            coordinates[19 * i + 14] = ag.getRotationRate().getY();
            coordinates[19 * i + 15] = ag.getRotationRate().getZ();

            coordinates[19 * i + 16] = ag.getRotationAcceleration().getX();
            coordinates[19 * i + 17] = ag.getRotationAcceleration().getY();
            coordinates[19 * i + 18] = ag.getRotationAcceleration().getZ();

        }

        return coordinates;

    }

    /** Internal class used only for serialization.
     * @since 4.0
     */
    private static class DataTransferObject implements Serializable {

        /** Serializable UID. */
        private static final long serialVersionUID = 20250427L;

        /** Inertial frame. */
        private final Predefined inertialFrame;

        /** Body frame. */
        private final Predefined bodyFrame;

        /** Start of search time span. */
        private final AbsoluteDate minDate;

        /** End of search time span. */
        private final AbsoluteDate maxDate;

        /** Step to use for inertial frame to body frame transforms cache computations. */
        private final double tStep;

        /** Tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting. */
        private final double overshootTolerance;

        /** Transforms sample from observed body frame to inertial frame. */
        private final long[] bodyToInertialTimeOffset;

        /** Transforms sample from observed body frame to inertial frame. */
        private final double[] bodyToInertialCoordinates;

        /** Transforms sample from spacecraft frame to inertial frame. */
        private final long[] scToInertialTimOffset;

        /** Transforms sample from spacecraft frame to inertial frame. */
        private final double[] scToInertialCoordinates;

        /** Simple constructor.
         * @param inertialFrame inertial frame
         * @param bodyFrame observed body frame
         * @param minDate start of search time span
         * @param maxDate end of search time span
         * @param tStep step to use for inertial frame to body frame transforms cache computations
         * @param overshootTolerance tolerance in seconds allowed for {@code minDate} and {@code maxDate} overshooting
         * slightly the position, velocity and quaternions ephemerides
         * @param bodyToInertialTimeOffset time offsets of transforms sample from observed body frame to inertial frame
         * @param bodyToInertialCoordinates coordinates of transforms sample from observed body frame to inertial frame
         * @param scToInertialTimOffset time offsets transforms sample from spacecraft frame to inertial frame
         * @param scToInertialCoordinates coordinates transforms sample from spacecraft frame to inertial frame
         */
        DataTransferObject(final Predefined inertialFrame, final Predefined bodyFrame,
                           final AbsoluteDate minDate, final AbsoluteDate maxDate, final double tStep,
                           final double overshootTolerance,
                           final long[] bodyToInertialTimeOffset, final double[] bodyToInertialCoordinates,
                           final long[] scToInertialTimOffset, final double[] scToInertialCoordinates) {
            this.inertialFrame             = inertialFrame;
            this.bodyFrame                 = bodyFrame;
            this.minDate                   = minDate;
            this.maxDate                   = maxDate;
            this.tStep                     = tStep;
            this.overshootTolerance        = overshootTolerance;
            this.bodyToInertialTimeOffset  = bodyToInertialTimeOffset;
            this.bodyToInertialCoordinates = bodyToInertialCoordinates;
            this.scToInertialTimOffset     = scToInertialTimOffset;
            this.scToInertialCoordinates   = scToInertialCoordinates;
        }

        /** Create a transform.
         * @param i index of the transfor
         * @param timeOffsets time offsets array
         * @param coordinates coordinates array
         * @return transform
         */
        private Transform createTransform(final int i,
                                          final long[] timeOffsets,
                                          final double[] coordinates) {
            final AbsoluteDate date = new AbsoluteDate(new TimeOffset(timeOffsets[2 * i],
                                                                      timeOffsets[2 * i + 1]));
            final PVCoordinates pv = new PVCoordinates(new Vector3D(coordinates[19 * i],
                                                                    coordinates[19 * i +  1],
                                                                    coordinates[19 * i +  2]),
                                                       new Vector3D(coordinates[19 * i +  3],
                                                                    coordinates[19 * i +  4],
                                                                    coordinates[19 * i +  5]),
                                                       new Vector3D(coordinates[19 * i +  6],
                                                                    coordinates[19 * i +  7],
                                                                    coordinates[19 * i +  8]));
            final AngularCoordinates ag = new AngularCoordinates(new Rotation(coordinates[19 * i +  9],
                                                                              coordinates[19 * i + 10],
                                                                              coordinates[19 * i + 11],
                                                                              coordinates[19 * i + 12],
                                                                              false),
                                                                 new Vector3D(coordinates[19 * i + 13],
                                                                              coordinates[19 * i + 14],
                                                                              coordinates[19 * i + 15]),
                                                                 new Vector3D(coordinates[19 * i + 16],
                                                                              coordinates[19 * i + 17],
                                                                              coordinates[19 * i + 18]));
            return new Transform(date, pv, ag);
        }

        /** Create all transforms.
         * @param timeOffsets time offsets array
         * @param coordinates coordinates array
         * @return all transforms
         */
        private List<Transform> createAllTransforms(final long[] timeOffsets,
                                                    final double[] coordinates) {
            final List<Transform> transforms = new ArrayList<>(timeOffsets.length / 2);
            for (int i = 0; i < timeOffsets.length / 2; ++i) {
                transforms.add(createTransform(i, timeOffsets, coordinates));
            }
            return transforms;
        }

        /** Replace the deserialized data transfer object with a
         * {@link SpacecraftToObservedBody}.
         * @return replacement {@link SpacecraftToObservedBody}
         */
        private Object readResolve() {
            return new SpacecraftToObservedBody(FramesFactory.getFrame(inertialFrame),
                                                FramesFactory.getFrame(bodyFrame),
                                                minDate, maxDate, tStep, overshootTolerance,
                                                createAllTransforms(bodyToInertialTimeOffset,
                                                                    bodyToInertialCoordinates),
                                                createAllTransforms(scToInertialTimOffset,
                                                                    scToInertialCoordinates));
        }

    }

}