CachedNormalizedSphericalHarmonicsProvider.java

/* Copyright 2002-2023 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.potential;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

import org.hipparchus.analysis.interpolation.HermiteInterpolator;
import org.orekit.errors.OrekitException;
import org.orekit.errors.TimeStampedCacheException;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeStamped;
import org.orekit.utils.GenericTimeStampedCache;
import org.orekit.utils.TimeStampedCache;
import org.orekit.utils.TimeStampedGenerator;

/** Caching wrapper for {@link NormalizedSphericalHarmonicsProvider}.
 * <p>
 * This wrapper improves efficiency of {@link NormalizedSphericalHarmonicsProvider}
 * by sampling the values at a user defined rate and using interpolation
 * between samples. This is important with providers that have sub-daily
 * frequencies and are computing intensive, such as tides fields.
 * </p>
 * @see NormalizedSphericalHarmonicsProvider
 * @see org.orekit.forces.gravity.SolidTides
 * @see TimeStampedCache
 * @author Luc Maisonobe
 * @since 6.1
 */
public class CachedNormalizedSphericalHarmonicsProvider implements NormalizedSphericalHarmonicsProvider {

    /** Underlying raw provider. */
    private final NormalizedSphericalHarmonicsProvider rawProvider;

    /** Number of coefficients in C<sub>n, m</sub> and S<sub>n, m</sub> arrays (counted separately). */
    private final int size;

    /** Cache. */
    private final TimeStampedCache<TimeStampedSphericalHarmonics> cache;

    /** Simple constructor.
     * @param rawProvider underlying raw provider
     * @param step time step between sample points for interpolation
     * @param nbPoints number of points to use for interpolation, must be at least 2
     * @param maxSlots maximum number of independent cached time slots
     * @param maxSpan maximum duration span in seconds of one slot
     * (can be set to {@code Double.POSITIVE_INFINITY} if desired)
     * @param newSlotInterval time interval above which a new slot is created
     * instead of extending an existing one
     */
    public CachedNormalizedSphericalHarmonicsProvider(final NormalizedSphericalHarmonicsProvider rawProvider,
                                                      final double step, final int nbPoints,
                                                      final int maxSlots, final double maxSpan,
                                                      final double newSlotInterval) {

        this.rawProvider  = rawProvider;
        final int k       = rawProvider.getMaxDegree() + 1;
        this.size         = (k * (k + 1)) / 2;

        cache = new GenericTimeStampedCache<TimeStampedSphericalHarmonics>(nbPoints, maxSlots, maxSpan,
                                                                           newSlotInterval, new Generator(step));
    }

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

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

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

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

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

    /** {@inheritDoc} */
    @Override
    public TideSystem getTideSystem() {
        return rawProvider.getTideSystem();
    }

    /** {@inheritDoc} */
    @Override
    public NormalizedSphericalHarmonics onDate(final AbsoluteDate date) {
        return TimeStampedSphericalHarmonics.interpolate(date, cache.getNeighbors(date));
    }

    /** Generator for time-stamped spherical harmonics. */
    private class Generator implements TimeStampedGenerator<TimeStampedSphericalHarmonics> {

        /** Time step between generated sets. */
        private final double step;

        /** Simple constructor.
         * @param step time step between generated sets
         */
        Generator(final double step) {
            this.step = step;
        }

        /** {@inheritDoc} */
        @Override
        public List<TimeStampedSphericalHarmonics> generate(final AbsoluteDate existingDate,
                                                            final AbsoluteDate date) {
            try {

                final List<TimeStampedSphericalHarmonics> generated =
                        new ArrayList<TimeStampedSphericalHarmonics>();
                final double[] cnmsnm = new double[2 * size];

                if (existingDate == null) {

                    // no prior existing transforms, just generate a first set
                    for (int i = 0; i < cache.getMaxNeighborsSize(); ++i) {
                        final AbsoluteDate t = date.shiftedBy((i - cache.getMaxNeighborsSize() / 2) * step);
                        fillArray(rawProvider.onDate(t), cnmsnm);
                        generated.add(new TimeStampedSphericalHarmonics(t, cnmsnm));
                    }

                } else {

                    // some coefficients have already been generated
                    // add the missing ones up to specified date

                    AbsoluteDate t = existingDate;
                    if (date.compareTo(t) > 0) {
                        // forward generation
                        do {
                            t = t.shiftedBy(step);
                            fillArray(rawProvider.onDate(t), cnmsnm);
                            generated.add(new TimeStampedSphericalHarmonics(t, cnmsnm));
                        } while (t.compareTo(date) <= 0);
                    } else {
                        // backward generation
                        do {
                            t = t.shiftedBy(-step);
                            fillArray(rawProvider.onDate(t), cnmsnm);
                            generated.add(new TimeStampedSphericalHarmonics(t, cnmsnm));
                        } while (t.compareTo(date) >= 0);
                        // ensure forward chronological order
                        Collections.reverse(generated);
                    }

                }

                // return the generated sample
                return generated;

            } catch (OrekitException oe) {
                throw new TimeStampedCacheException(oe);
            }
        }

        /** Fill coefficients array for one entry.
         * @param raw the un-interpolated spherical harmonics
         * @param cnmsnm arrays to fill in
         */
        private void fillArray(final NormalizedSphericalHarmonics raw,
                               final double[] cnmsnm) {
            int index = 0;
            for (int n = 0; n <= rawProvider.getMaxDegree(); ++n) {
                for (int m = 0; m <= n; ++m) {
                    cnmsnm[index++] = raw.getNormalizedCnm(n, m);
                }
            }
            for (int n = 0; n <= rawProvider.getMaxDegree(); ++n) {
                for (int m = 0; m <= n; ++m) {
                    cnmsnm[index++] = raw.getNormalizedSnm(n, m);
                }
            }
        }

    }

    /**
     * Internal class for time-stamped spherical harmonics. Instances are created using
     * {@link #interpolate(AbsoluteDate, Collection)}
     */
    private static class TimeStampedSphericalHarmonics
            implements TimeStamped, NormalizedSphericalHarmonics {

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

        /** number of C or S coefficients. */
        private final int size;

        /** Flattened array for C<sub>n,m</sub> and S<sub>n,m</sub> coefficients. */
        private final double[] cnmsnm;

        /** Simple constructor.
         * @param date current date
         * @param cnmsnm flattened array for C<sub>n,m</sub> and S<sub>n,m</sub>
         *               coefficients. It is copied.
         */
        private TimeStampedSphericalHarmonics(final AbsoluteDate date,
                                              final double[] cnmsnm) {
            this.date   = date;
            this.cnmsnm = cnmsnm.clone();
            this.size   = cnmsnm.length / 2;
        }

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

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

        /** {@inheritDoc} */
        @Override
        public double getNormalizedSnm(final int n, final int m) {
            return cnmsnm[(n * (n + 1)) / 2 + m + size];
        }

        /** Interpolate spherical harmonics.
         * <p>
         * The interpolated instance is created by polynomial Hermite interpolation.
         * </p>
         * @param date interpolation date
         * @param sample sample points on which interpolation should be done
         * @return a new time-stamped spherical harmonics, interpolated at specified date
         */
        public static TimeStampedSphericalHarmonics interpolate(final AbsoluteDate date,
                                                                final Stream<TimeStampedSphericalHarmonics> sample) {

            // set up an interpolator taking derivatives into account
            final HermiteInterpolator interpolator = new HermiteInterpolator();

            // add sample points
            sample.forEach(tssh -> interpolator.addSamplePoint(tssh.date.durationFrom(date), tssh.cnmsnm));

            // build a new interpolated instance
            return new TimeStampedSphericalHarmonics(date, interpolator.value(0.0));

        }

    }

}