AbstractSolarActivityData.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.models.earth.atmosphere.data;

import org.hipparchus.exception.DummyLocalizable;
import org.orekit.data.DataProvidersManager;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.models.earth.atmosphere.DTM2000InputParameters;
import org.orekit.models.earth.atmosphere.NRLMSISE00InputParameters;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeStamped;
import org.orekit.utils.GenericTimeStampedCache;
import org.orekit.utils.ImmutableTimeStampedCache;
import org.orekit.utils.TimeStampedGenerator;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Abstract class for solar activity data.
 *
 * @param <L> type of the line parameters
 * @param <D> type of the solar activity data loader
 *
 * @author Vincent Cucchietti
 * @since 12.0
 */
public abstract class AbstractSolarActivityData<L extends AbstractSolarActivityDataLoader.LineParameters, D extends AbstractSolarActivityDataLoader<L>>
        implements DTM2000InputParameters, NRLMSISE00InputParameters {

    /** Size of the list. */
    protected static final int N_NEIGHBORS = 2;

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

    /** Weather data thread safe cache. */
    private final transient GenericTimeStampedCache<L> cache;

    /** Supported names. */
    private final String supportedNames;

    /** UTC time scale. */
    private final TimeScale utc;

    /** First available date. */
    private final AbsoluteDate firstDate;

    /** Last available date. */
    private final AbsoluteDate lastDate;

    /**
     * @param supportedNames regular expression for supported AGI/CSSI space weather files names
     * @param loader data loader
     * @param dataProvidersManager provides access to auxiliary data files.
     * @param utc UTC time scale
     * @param maxSlots maximum number of independent cached time slots in the
     * {@link GenericTimeStampedCache time-stamped cache}
     * @param maxSpan maximum duration span in seconds of one slot in the {@link GenericTimeStampedCache time-stamped cache}
     * @param maxInterval time interval above which a new slot is created in the
     * {@link GenericTimeStampedCache time-stamped cache}
     * @param minimumStep overriding minimum step designed for non-homogeneous tabulated values. To be used for example when
     * caching monthly tabulated values. May be null.
     */
    protected AbstractSolarActivityData(final String supportedNames, final D loader,
                                        final DataProvidersManager dataProvidersManager, final TimeScale utc,
                                        final int maxSlots, final double maxSpan, final double maxInterval,
                                        final double minimumStep) {
        // Load data
        dataProvidersManager.feed(supportedNames, loader);

        // Create thread safe cache
        this.cache = new GenericTimeStampedCache<>(N_NEIGHBORS, maxSlots, maxSpan, maxInterval,
                                                   new SolarActivityGenerator(loader.getDataSet()), minimumStep);

        // Initialise fields
        this.supportedNames = supportedNames;
        this.utc            = utc;
        this.firstDate      = loader.getMinDate();
        this.lastDate       = loader.getMaxDate();
    }

    /**
     * Simple constructor.
     *
     * @param source source for the data
     * @param loader data loader
     * @param utc UTC time scale
     * @param maxSlots maximum number of independent cached time slots in the
     * {@link GenericTimeStampedCache time-stamped cache}
     * @param maxSpan maximum duration span in seconds of one slot in the {@link GenericTimeStampedCache time-stamped cache}
     * @param maxInterval time interval above which a new slot is created in the
     * {@link GenericTimeStampedCache time-stamped cache}
     * @param minimumStep overriding minimum step designed for non-homogeneous tabulated values. To be used for example when
     * caching monthly tabulated values. May be null.
     *
     * @since 12.0
     */
    public AbstractSolarActivityData(final DataSource source, final D loader, final TimeScale utc, final int maxSlots,
                                     final double maxSpan, final double maxInterval, final double minimumStep) {
        try {
            // Load file
            try (InputStream is = source.getOpener().openStreamOnce();
                    BufferedInputStream bis = new BufferedInputStream(is)) {
                loader.loadData(bis, source.getName());
            }

            // Create thread safe cache
            this.cache = new GenericTimeStampedCache<>(N_NEIGHBORS, maxSlots, maxSpan, maxInterval,
                                                       new SolarActivityGenerator(loader.getDataSet()), minimumStep);

            // Initialise fields
            this.supportedNames = source.getName();
            this.utc            = utc;
            this.firstDate      = loader.getMinDate();
            this.lastDate       = loader.getMaxDate();
        }
        catch (IOException | ParseException ioe) {
            throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
        }
    }

    /**
     * Performs a linear interpolation between two values The weights are computed from the time delta between previous date,
     * current date, next date.
     *
     * @param date current date
     * @param solarActivityToDoubleMapper mapping function taking solar activity as input and returning a double
     *
     * @return the value interpolated for the current date
     */
    protected double getLinearInterpolation(final AbsoluteDate date, final Function<L, Double> solarActivityToDoubleMapper) {
        // Create solar activity around current date
        final LocalSolarActivity localSolarActivity = new LocalSolarActivity(date);

        // Extract values
        return getLinearInterpolation(localSolarActivity, solarActivityToDoubleMapper);
    }

    /**
     * Performs a linear interpolation between two values The weights are computed from the time delta between previous date,
     * current date, next date.
     *
     * @param localSolarActivity solar activity around current date
     * @param solarActivityToDoubleMapper mapping function taking solar activity as input and returning a double
     *
     * @return the value interpolated for the current date
     */
    protected double getLinearInterpolation(final LocalSolarActivity localSolarActivity,
                                            final Function<L, Double> solarActivityToDoubleMapper) {
        // Extract values
        final L      previousParameters = localSolarActivity.getPreviousParam();
        final double previousValue      = solarActivityToDoubleMapper.apply(previousParameters);

        final L      nextParameters = localSolarActivity.getNextParam();
        final double nextValue      = solarActivityToDoubleMapper.apply(nextParameters);

        // Perform a linear interpolation
        final AbsoluteDate previousDate   = localSolarActivity.getPreviousParam().getDate();
        final AbsoluteDate currentDate    = localSolarActivity.getNextParam().getDate();
        final double       dt             = currentDate.durationFrom(previousDate);
        final AbsoluteDate date           = localSolarActivity.getDate();
        final double       previousWeight = currentDate.durationFrom(date) / dt;
        final double       nextWeight     = date.durationFrom(previousDate) / dt;

        // Returns the data interpolated at the date
        return previousValue * previousWeight + nextValue * nextWeight;
    }

    /**
     * Get underlying cache.
     *
     * @return cache
     */
    public GenericTimeStampedCache<L> getCache() {
        return cache;
    }

    /**
     * Get the supported names regular expression.
     *
     * @return the supported names.
     */
    public String getSupportedNames() {
        return supportedNames;
    }

    /**
     * Get the UTC timescale.
     *
     * @return UTC timescale
     */
    public TimeScale getUTC() {
        return utc;
    }

    /** {@inheritDoc} */
    @Override
    public AbsoluteDate getMinDate() {
        return firstDate;
    }

    /** {@inheritDoc} */
    @Override
    public AbsoluteDate getMaxDate() {
        return lastDate;
    }

    /** Container for weather parameters around current date. Allows for thread safe use. */
    protected class LocalSolarActivity implements TimeStamped {

        /** Date. */
        private final AbsoluteDate currentDate;

        /** Previous parameters. */
        private final L previousParam;

        /** Next parameters. */
        private final L nextParam;

        /**
         * Constructor.
         *
         * @param date current date
         */
        public LocalSolarActivity(final AbsoluteDate date) {
            // Asked date is before earliest available data
            if (date.durationFrom(firstDate) < 0) {
                throw new OrekitException(OrekitMessages.OUT_OF_RANGE_EPHEMERIDES_DATE_BEFORE, date, firstDate, lastDate,
                                          firstDate.durationFrom(date));
            }
            // Asked date is after latest available data
            if (date.durationFrom(lastDate) > 0) {
                throw new OrekitException(OrekitMessages.OUT_OF_RANGE_EPHEMERIDES_DATE_AFTER, date, firstDate, lastDate,
                                          date.durationFrom(lastDate));
            }

            final List<L> neighbours = cache.getNeighbors(date).collect(Collectors.toList());

            this.currentDate   = date;
            this.previousParam = neighbours.get(0);
            this.nextParam     = neighbours.get(1);
        }

        /** @return current date */
        public AbsoluteDate getDate() {
            return currentDate;
        }

        /** @return previous parameters */
        public L getPreviousParam() {
            return previousParam;
        }

        /** @return next parameters */
        public L getNextParam() {
            return nextParam;
        }
    }

    /** Generator used in the weather data cache. */
    protected class SolarActivityGenerator implements TimeStampedGenerator<L> {

        /**
         * Default time step to shift the date.
         * <p>
         * It is used so that, in the case where the earliest date is exactly at noon, we do not get the following interval
         * [previous day; current day] but rather the expected interval [current day; next day]
         */
        private static final double STEP = 1;

        /** Data set. */
        private final ImmutableTimeStampedCache<L> data;

        /**
         * Constructor.
         *
         * @param dataSet weather data
         */
        protected SolarActivityGenerator(final Collection<L> dataSet) {
            this.data = new ImmutableTimeStampedCache<>(N_NEIGHBORS, dataSet);
        }

        /** {@inheritDoc} */
        @Override
        public List<L> generate(final AbsoluteDate existingDate, final AbsoluteDate date) {
            // No prior data in the cache
            if (existingDate == null) {
                return data.getNeighbors(date).collect(Collectors.toList());
            }
            // Prior data in the cache, fill with data between date and existing date
            if (date.isBefore(existingDate)) {
                return generateDataFromEarliestToLatestDates(date, existingDate);
            }
            return generateDataFromEarliestToLatestDates(existingDate, date);
        }

        /**
         * Generate a list of parameters between earliest and latest dates.
         *
         * @param earliest earliest date
         * @param latest latest date
         *
         * @return list of parameters between earliest and latest dates
         */
        public List<L> generateDataFromEarliestToLatestDates(final AbsoluteDate earliest, final AbsoluteDate latest) {
            /* Gives first two parameters bracketing the earliest date
             * A date shifted by step is used so that, in the case where the earliest date is exactly noon, we do not get the
             * following interval [previous day; current day] but rather the expected interval [current day; next day] */
            List<L> neighbours = data.getNeighbors(earliest.shiftedBy(STEP)).collect(Collectors.toList());

            // Get next parameter until it brackets the latest date
            AbsoluteDate  latestNeighbourDate = neighbours.get(1).getDate();
            final List<L> params              = new ArrayList<>(neighbours);
            while (latestNeighbourDate.isBefore(latest)) {
                neighbours = data.getNeighbors(latestNeighbourDate.shiftedBy(STEP)).collect(Collectors.toList());
                params.add(neighbours.get(1));
                latestNeighbourDate = neighbours.get(1).getDate();
            }
            return params;
        }
    }
}