PrecessionFinder.java

/* Copyright 2023 Luc Maisonobe
 * 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.files.ccsds.ndm.adm;

import org.hipparchus.analysis.differentiation.UnivariateDerivative2;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.SinCos;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;

/** Utility to extract precession from the motion of a spin axis.
 * <p>
 * The precession is used in {@link AttitudeType#SPIN_NUTATION} and
 * {@link AttitudeType#SPIN_NUTATION_MOMENTUM} attitude types. CCSDS
 * calls it nutation, but it is really precession.
 * </p>
 * @author Luc Maisonobe
 * @since 12.0
 */
class PrecessionFinder {

    /** Precession axis (axis of the cone described by spin). */
    private final Vector3D axis;

    /** Precession angle (fixed cone angle between precession axis and spin axis). */
    private final double precessionAngle;

    /** Angular velocity around the precession axis (rad/s). */
    private final double angularVelocity;

    /** Build from spin axis motion.
     * <p>
     * Note that the derivatives up to second order are really needed
     * in order to retrieve the precession motion.
     * </p>
     * @param spin spin axis, including value, first and second derivative
     */
    PrecessionFinder(final FieldVector3D<UnivariateDerivative2> spin) {

        // using a suitable coordinates frame, the spin axis can be considered to describe
        // a cone of half aperture angle 0 ≤ η ≤ π around k axis, at angular rate ω ≥ 0
        // with an initial position in the (+i,±k) half-plane. In this frame, the normalized
        // direction of spin s = spin/||spin|| and its first and second time derivatives
        // have coordinates:
        //           s(t):        (sin η cos ω(t-t₀), sin η sin ω(t-t₀), cos η)
        //           ds/dt(t):    (-ω sin η sin ω(t-t₀), ω sin η cos ω(t-t₀), 0)
        //           d²s/dt²(t):  (-ω² sin η cos ω(t-t₀), -ω² sin η sin ω(t-t₀), 0)
        // at initial time t = t₀ this leads to:
        //           s₀ = s(t₀):       (sin η, 0, cos η)
        //           s₁ = ds/dt(t₀):   (0, ω sin η, 0)
        //           s₂ = d²s/dt²(t₀): (-ω² sin η, 0, 0)
        // however, only the spin vector itself is provided, we don't initially know the unit
        // vectors (i, j, k) of this "suitable coordinates frame". We can however easily
        // build another frame (u, v, w) from the normalized spin vector s as follows:
        //           u = s₀, v = s₁/||s₁||, w = u ⨯ v
        // the coordinates of vectors u, v and w in the "suitable coordinates frame" are:
        //           u: ( sin η,   0,  cos η)
        //           v: (     0,   1,      0)
        //           w: (-cos η,   0,  sin η)
        // we can then deduce the following relations, which can be computed regardless
        // of frames used to express the various vectors:
        //           s₁ · v = ω  sin η  = ||s₁||
        //           s₂ · w = ω² sin η cos η
        // these relations can be solved for η and ω (we know that 0 ≤ η ≤ π and ω ≥ 0):
        //           η = atan2(||s₁||², s₂ · w)
        //           ω = ||s₁|| / sin  η
        // then the k axis, which is the precession axis, can be deduced as:
        //           k = cos η u + sin η w

        final UnivariateDerivative2 nS = spin.getNorm();
        if (nS.getValue() == 0) {
            // special case, no motion at all, set up arbitrary values
            axis            = Vector3D.PLUS_K;
            precessionAngle = 0.0;
            angularVelocity = 0.0;
        } else {

            // build the derivatives vectors
            final FieldVector3D<UnivariateDerivative2> s = spin.scalarMultiply(nS.reciprocal());
            final Vector3D s0 = new Vector3D(s.getX().getValue(),
                                             s.getY().getValue(),
                                             s.getZ().getValue());
            final Vector3D s1 = new Vector3D(s.getX().getFirstDerivative(),
                                             s.getY().getFirstDerivative(),
                                             s.getZ().getFirstDerivative());
            final Vector3D s2 = new Vector3D(s.getX().getSecondDerivative(),
                                             s.getY().getSecondDerivative(),
                                             s.getZ().getSecondDerivative());

            final double nV2 = s1.getNormSq();
            if (nV2 == 0.0) {
                // special case: we have a fixed spin vector
                axis            = s0;
                precessionAngle = 0.0;
                angularVelocity = 0.0;
            } else {

                // check second derivatives were provided ; we do it on spin rather than s2
                // and use test against exact 0.0 because the normalization process changes
                // the derivatives and what we really want to check are missing derivatives
                if (new Vector3D(spin.getX().getSecondDerivative(),
                                 spin.getY().getSecondDerivative(),
                                 spin.getZ().getSecondDerivative()).getNormSq() == 0) {
                    throw new OrekitException(OrekitMessages.CANNOT_ESTIMATE_PRECESSION_WITHOUT_PROPER_DERIVATIVES);
                }

                // build the unit vectors
                final double   nV = FastMath.sqrt(nV2);
                final Vector3D v  = s1.scalarMultiply(1.0 / nV);
                final Vector3D w  = Vector3D.crossProduct(s0, v);

                // compute precession model
                precessionAngle  = FastMath.atan2(nV2, Vector3D.dotProduct(s2, w));
                final SinCos sc  = FastMath.sinCos(precessionAngle);
                angularVelocity  = nV / sc.sin();
                axis             = new Vector3D(sc.cos(), s0, sc.sin(), w);

            }
        }

    }

    /** Get the precession axis.
     * @return precession axis
     */
    public Vector3D getAxis() {
        return axis;
    }

    /** Get the precession angle.
     * @return fixed cone angle between precession axis an spin axis (rad)
     */
    public double getPrecessionAngle() {
        return precessionAngle;
    }

    /** Get the angular velocity around precession axis.
     * @return angular velocity around precession axis (rad/s)
     */
    public double getAngularVelocity() {
        return angularVelocity;
    }

}