SolidTidesField.java

/* Copyright 2002-2024 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.forces.gravity;

import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.orekit.bodies.CelestialBody;
import org.orekit.forces.gravity.potential.NormalizedSphericalHarmonicsProvider;
import org.orekit.forces.gravity.potential.TideSystem;
import org.orekit.frames.Frame;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeVectorFunction;
import org.orekit.utils.LoveNumbers;

/** Gravity field corresponding to solid tides.
 * <p>
 * This solid tides force model implementation corresponds to the method described
 * in <a href="http://www.iers.org/nn_11216/IERS/EN/Publications/TechnicalNotes/tn36.html">
 * IERS conventions (2010)</a>, chapter 6, section 6.2.
 * </p>
 * <p>
 * The computation of the spherical harmonics part is done using the algorithm
 * designed by S. A. Holmes and W. E. Featherstone from Department of Spatial Sciences,
 * Curtin University of Technology, Perth, Australia and described in their 2002 paper:
 * <a href="https://www.researchgate.net/publication/226460594_A_unified_approach_to_the_Clenshaw_summation_and_the_recursive_computation_of_very_high_degree_and_order_normalised_associated_Legendre_functions">
 * A unified approach to the Clenshaw summation and the recursive computation of
 * very high degree and order normalised associated Legendre functions</a>
 * (Journal of Geodesy (2002) 76: 279–299).
 * </p>
 * <p>
 * Note that this class is <em>not</em> thread-safe, and that tides computation
 * are computer intensive if repeated. So this class is really expected to
 * be wrapped within a {@link
 * org.orekit.forces.gravity.potential.CachedNormalizedSphericalHarmonicsProvider}
 * that will both ensure thread safety and improve performances using caching.
 * </p>
 * @see SolidTides
 * @author Luc Maisonobe
 * @since 6.1
 */
class SolidTidesField implements NormalizedSphericalHarmonicsProvider {

    /** Maximum degree for normalized Legendre functions. */
    private static final int MAX_LEGENDRE_DEGREE = 4;

    /** Love numbers. */
    private final LoveNumbers love;

    /** Function computing frequency dependent terms (ΔC₂₀, ΔC₂₁, ΔS₂₁, ΔC₂₂, ΔS₂₂). */
    private final TimeVectorFunction deltaCSFunction;

    /** Permanent tide to be <em>removed</em> from ΔC₂₀ when zero-tide potentials are used. */
    private final double deltaC20PermanentTide;

    /** Function computing pole tide terms (ΔC₂₁, ΔS₂₁). */
    private final TimeVectorFunction poleTideFunction;

    /** Rotating body frame. */
    private final Frame centralBodyFrame;

    /** Central body reference radius. */
    private final double ae;

    /** Central body attraction coefficient. */
    private final double mu;

    /** Tide system used in the central attraction model. */
    private final TideSystem centralTideSystem;

    /** Tide generating bodies (typically Sun and Moon). Read only after construction. */
    private final CelestialBody[] bodies;

    /** First recursion coefficients for tesseral terms. Read only after construction. */
    private final double[][] anm;

    /** Second recursion coefficients for tesseral terms. Read only after construction. */
    private final double[][] bnm;

    /** Third recursion coefficients for sectorial terms. Read only after construction. */
    private final double[] dmm;

    /** Simple constructor.
     * @param love Love numbers
     * @param deltaCSFunction function computing frequency dependent terms (ΔC₂₀, ΔC₂₁, ΔS₂₁, ΔC₂₂, ΔS₂₂)
     * @param deltaC20PermanentTide permanent tide to be <em>removed</em> from ΔC₂₀ when zero-tide potentials are used
     * @param poleTideFunction function computing pole tide terms (ΔC₂₁, ΔS₂₁), may be null
     * @param centralBodyFrame rotating body frame
     * @param ae central body reference radius
     * @param mu central body attraction coefficient
     * @param centralTideSystem tide system used in the central attraction model
     * @param bodies tide generating bodies (typically Sun and Moon)
     */
    SolidTidesField(final LoveNumbers love, final TimeVectorFunction deltaCSFunction,
                           final double deltaC20PermanentTide, final TimeVectorFunction poleTideFunction,
                           final Frame centralBodyFrame, final double ae, final double mu,
                           final TideSystem centralTideSystem, final CelestialBody... bodies) {

        // store mode parameters
        this.centralBodyFrame  = centralBodyFrame;
        this.ae                = ae;
        this.mu                = mu;
        this.centralTideSystem = centralTideSystem;
        this.bodies            = bodies;

        // compute recursion coefficients for Legendre functions
        this.anm               = buildTriangularArray(5, false);
        this.bnm               = buildTriangularArray(5, false);
        this.dmm               = new double[love.getSize()];
        recursionCoefficients();

        // Love numbers
        this.love = love;

        // frequency dependent terms
        this.deltaCSFunction = deltaCSFunction;

        // permanent tide
        this.deltaC20PermanentTide = deltaC20PermanentTide;

        // pole tide
        this.poleTideFunction = poleTideFunction;

    }

    /** {@inheritDoc} */
    @Override
    public int getMaxDegree() {
        return MAX_LEGENDRE_DEGREE;
    }

    /** {@inheritDoc} */
    @Override
    public int getMaxOrder() {
        return MAX_LEGENDRE_DEGREE;
    }

    /** {@inheritDoc} */
    @Override
    public double getMu() {
        return mu;
    }

    /** {@inheritDoc} */
    @Override
    public double getAe() {
        return ae;
    }

    /** {@inheritDoc} */
    @Override
    public AbsoluteDate getReferenceDate() {
        return AbsoluteDate.ARBITRARY_EPOCH;
    }

    /** {@inheritDoc} */
    @Override
    public TideSystem getTideSystem() {
        // not really used here, but for consistency we can state that either
        // we add the permanent tide or it was already in the central attraction
        return TideSystem.ZERO_TIDE;
    }

    /** {@inheritDoc} */
    @Override
    public NormalizedSphericalHarmonics onDate(final AbsoluteDate date) {

        // computed Cnm and Snm coefficients
        final double[][] cnm = buildTriangularArray(5, true);
        final double[][] snm = buildTriangularArray(5, true);

        // work array to hold Legendre coefficients
        final double[][] pnm = buildTriangularArray(5, true);

        // step 1: frequency independent part
        // equations 6.6 (for degrees 2 and 3) and 6.7 (for degree 4) in IERS conventions 2010
        for (final CelestialBody body : bodies) {

            // compute tide generating body state
            final Vector3D position = body.getPosition(date, centralBodyFrame);

            // compute polar coordinates
            final double x    = position.getX();
            final double y    = position.getY();
            final double z    = position.getZ();
            final double x2   = x * x;
            final double y2   = y * y;
            final double z2   = z * z;
            final double r2   = x2 + y2 + z2;
            final double r    = FastMath.sqrt (r2);
            final double rho2 = x2 + y2;
            final double rho  = FastMath.sqrt(rho2);

            // evaluate Pnm
            evaluateLegendre(z / r, rho / r, pnm);

            // update spherical harmonic coefficients
            frequencyIndependentPart(r, body.getGM(), x / rho, y / rho, pnm, cnm, snm);

        }

        // step 2: frequency dependent corrections
        frequencyDependentPart(date, cnm, snm);

        if (centralTideSystem == TideSystem.ZERO_TIDE) {
            // step 3: remove permanent tide which is already considered
            // in the central body gravity field
            removePermanentTide(cnm);
        }

        if (poleTideFunction != null) {
            // add pole tide
            poleTide(date, cnm, snm);
        }

        return new TideHarmonics(date, cnm, snm);

    }

    /** Compute recursion coefficients. */
    private void recursionCoefficients() {

        // pre-compute the recursion coefficients
        // (see equations 11 and 12 from Holmes and Featherstone paper)
        for (int n = 0; n < anm.length; ++n) {
            for (int m = 0; m < n; ++m) {
                final double f = (n - m) * (n + m );
                anm[n][m] = FastMath.sqrt((2 * n - 1) * (2 * n + 1) / f);
                bnm[n][m] = FastMath.sqrt((2 * n + 1) * (n + m - 1) * (n - m - 1) / (f * (2 * n - 3)));
            }
        }

        // sectorial terms corresponding to equation 13 in Holmes and Featherstone paper
        dmm[0] = Double.NaN; // dummy initialization for unused term
        dmm[1] = Double.NaN; // dummy initialization for unused term
        for (int m = 2; m < dmm.length; ++m) {
            dmm[m] = FastMath.sqrt((2 * m + 1) / (2.0 * m));
        }

    }

    /** Evaluate Legendre functions.
     * @param t cos(θ), where θ is the polar angle
     * @param u sin(θ), where θ is the polar angle
     * @param pnm the computed coefficients. Modified in place.
     */
    private void evaluateLegendre(final double t, final double u, final double[][] pnm) {

        // as the degree is very low, we use the standard forward column method
        // and store everything (see equations 11 and 13 from Holmes and Featherstone paper)
        pnm[0][0] = 1;
        pnm[1][0] = anm[1][0] * t;
        pnm[1][1] = FastMath.sqrt(3) * u;
        for (int m = 2; m < dmm.length; ++m) {
            pnm[m][m - 1] = anm[m][m - 1] * t * pnm[m - 1][m - 1];
            pnm[m][m]     = dmm[m] * u * pnm[m - 1][m - 1];
        }
        for (int m = 0; m < dmm.length; ++m) {
            for (int n = m + 2; n < pnm.length; ++n) {
                pnm[n][m] = anm[n][m] * t * pnm[n - 1][m] - bnm[n][m] * pnm[n - 2][m];
            }
        }

    }

    /** Update coefficients applying frequency independent step, for one tide generating body.
     * @param r distance to tide generating body
     * @param gm tide generating body attraction coefficient
     * @param cosLambda cosine of the tide generating body longitude
     * @param sinLambda sine of the tide generating body longitude
     * @param pnm the Legendre coefficients. See {@link #evaluateLegendre(double, double, double[][])}.
     * @param cnm the computed Cnm coefficients. Modified in place.
     * @param snm the computed Snm coefficients. Modified in place.
     */
    private void frequencyIndependentPart(final double r,
                                          final double gm,
                                          final double cosLambda,
                                          final double sinLambda,
                                          final double[][] pnm,
                                          final double[][] cnm,
                                          final double[][] snm) {

        final double rRatio = ae / r;
        double fM           = gm / mu;
        double cosMLambda   = 1;
        double sinMLambda   = 0;
        for (int m = 0; m < love.getSize(); ++m) {

            double fNPlus1 = fM;
            for (int n = m; n < love.getSize(); ++n) {
                fNPlus1 *= rRatio;
                final double coeff = (fNPlus1 / (2 * n + 1)) * pnm[n][m];
                final double cCos  = coeff * cosMLambda;
                final double cSin  = coeff * sinMLambda;

                // direct effect of degree n tides on degree n coefficients
                // equation 6.6 from IERS conventions (2010)
                final double kR = love.getReal(n, m);
                final double kI = love.getImaginary(n, m);
                cnm[n][m] += kR * cCos + kI * cSin;
                snm[n][m] += kR * cSin - kI * cCos;

                if (n == 2) {
                    // indirect effect of degree 2 tides on degree 4 coefficients
                    // equation 6.7 from IERS conventions (2010)
                    final double kP = love.getPlus(n, m);
                    cnm[4][m] += kP * cCos;
                    snm[4][m] += kP * cSin;
                }

            }

            // prepare next iteration on order
            final double tmp = cosMLambda * cosLambda - sinMLambda * sinLambda;
            sinMLambda = sinMLambda * cosLambda + cosMLambda * sinLambda;
            cosMLambda = tmp;
            fM        *= rRatio;

        }

    }

    /** Update coefficients applying frequency dependent step.
     * @param date current date
     * @param cnm the Cnm coefficients. Modified in place.
     * @param snm the Snm coefficients. Modified in place.
     */
    private void frequencyDependentPart(final AbsoluteDate date,
                                        final double[][] cnm,
                                        final double[][] snm) {
        final double[] deltaCS = deltaCSFunction.value(date);
        cnm[2][0] += deltaCS[0]; // ΔC₂₀
        cnm[2][1] += deltaCS[1]; // ΔC₂₁
        snm[2][1] += deltaCS[2]; // ΔS₂₁
        cnm[2][2] += deltaCS[3]; // ΔC₂₂
        snm[2][2] += deltaCS[4]; // ΔS₂₂
    }

    /** Remove the permanent tide already counted in zero-tide central gravity fields.
     * @param cnm the Cnm coefficients. Modified in place.
     */
    private void removePermanentTide(final double[][] cnm) {
        cnm[2][0] -= deltaC20PermanentTide;
    }

    /** Update coefficients applying pole tide.
     * @param date current date
     * @param cnm the Cnm coefficients. Modified in place.
     * @param snm the Snm coefficients. Modified in place.
     */
    private void poleTide(final AbsoluteDate date,
                          final double[][] cnm,
                          final double[][] snm) {
        final double[] deltaCS = poleTideFunction.value(date);
        cnm[2][1] += deltaCS[0]; // ΔC₂₁
        snm[2][1] += deltaCS[1]; // ΔS₂₁
    }

    /** Create a triangular array.
     * @param size array size
     * @param withDiagonal if true, the array contains a[p][p] terms, otherwise each
     * row ends up at a[p][p-1]
     * @return new triangular array
     */
    private double[][] buildTriangularArray(final int size, final boolean withDiagonal) {
        final double[][] array = new double[size][];
        for (int i = 0; i < array.length; ++i) {
            array[i] = new double[withDiagonal ? i + 1 : i];
        }
        return array;
    }

    /** The Tidal geopotential evaluated on a specific date. */
    private static class TideHarmonics implements NormalizedSphericalHarmonics {

        /** evaluation date. */
        private final AbsoluteDate date;

        /** Cached cnm. */
        private final double[][] cnm;

        /** Cached snm. */
        private final double[][] snm;

        /** Construct the tidal harmonics on the given date.
         *
         * @param date of evaluation
         * @param cnm the Cnm coefficients. Not copied.
         * @param snm the Snm coeffiecients. Not copied.
         */
        private TideHarmonics(final AbsoluteDate date,
                              final double[][] cnm,
                              final double[][] snm) {
            this.date = date;
            this.cnm = cnm;
            this.snm = snm;
        }

        /** {@inheritDoc} */
        @Override
        public AbsoluteDate getDate() {
            return date;
        }

        /** {@inheritDoc} */
        @Override
        public double getNormalizedCnm(final int n, final int m) {
            return cnm[n][m];
        }

        /** {@inheritDoc} */
        @Override
        public double getNormalizedSnm(final int n, final int m) {
            return snm[n][m];
        }

    }

}