SatelliteClockScale.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.time;

import org.hipparchus.CalculusFieldElement;

/** Scale for on-board clock.
 * @author Luc Maisonobe
 * @since 11.0
 */
public class SatelliteClockScale implements TimeScale {

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

    /** Name of the scale. */
    private final String name;

    /** Reference epoch. */
    private final AbsoluteDate epoch;

    /** Reference epoch. */
    private final DateTimeComponents epochDT;

    /** Offset from TAI at epoch. */
    private final TimeOffset offsetAtEpoch;

    /** Clock count at epoch. */
    private final double countAtEpoch;

    /** Clock drift (i.e. clock count per SI second minus 1.0). */
    private final double drift;

    /** Clock rate. */
    private final double rate;

    /** Create a linear model for satellite clock.
     * <p>
     * Beware that we specify the model using its drift with respect to
     * flow of time. For a perfect clock without any drift, the clock
     * count would be one tick every SI second. A clock that is fast, say
     * for example it generates 1000001 ticks every 1000000 SI second, would
     * have a rate of 1.000001 tick per SI second and hence a drift of
     * 1.0e-6 tick per SI second. In this constructor we use the drift
     * (1.0e-6 in the previous example) rather than the rate (1.000001
     * in the previous example) to specify the clock. The rationale is
     * that for clocks that are intended to be used for representing absolute
     * time, the drift is expected to be small (much smaller that 1.0e-6
     * for a good clock), so using drift is numerically more stable than
     * using rate and risking catastrophic cancellation when subtracting
     * 1.0 in the internal computation.
     * </p>
     * <p>
     * Despite what is explained in the previous paragraph, this class can
     * handle spacecraft clocks that are not intended to be synchronized with
     * SI seconds, for example clocks that ticks at 10 Hz. In such cases the
     * drift would need to be set at 10.0 - 1.0 = 9.0, which is not intuitive.
     * For these clocks, the methods {@link #countAtDate(AbsoluteDate)} and
     * {@link #dateAtCount(double)} and perhaps {@link #offsetFromTAI(AbsoluteDate)}
     * are still useful, whereas {@link #offsetToTAI(DateComponents, TimeComponents)}
     * is probably not really meaningful.
     * </p>
     * @param name of the scale
     * @param epoch reference epoch
     * @param epochScale time scale in which the {@code epoch} was defined
     * @param countAtEpoch clock count at {@code epoch}
     * @param drift clock drift rate (i.e. clock count change per SI second minus 1.0)
     */
    public SatelliteClockScale(final String name,
                               final AbsoluteDate epoch, final TimeScale epochScale,
                               final double countAtEpoch, final double drift) {
        this.name          = name;
        this.epoch         = epoch;
        this.epochDT       = epoch.getComponents(epochScale);
        this.offsetAtEpoch = epochScale.offsetFromTAI(epoch).add(new TimeOffset(countAtEpoch));
        this.countAtEpoch  = countAtEpoch;
        this.drift         = drift;
        this.rate          = 1.0 + drift;
    }

    /** {@inheritDoc} */
    @Override
    public TimeOffset offsetFromTAI(final AbsoluteDate date) {
        return offsetAtEpoch.add(new TimeOffset(drift * date.durationFrom(epoch)));
    }

    /** {@inheritDoc} */
    @Override
    public TimeOffset offsetToTAI(final DateComponents date, final TimeComponents time) {
        final long      deltaDate      = (date.getJ2000Day() - epochDT.getDate().getJ2000Day()) *
                                         TimeOffset.DAY.getSeconds();
        final TimeOffset deltaTime      = time.getSplitSecondsInUTCDay().
                                         subtract(epochDT.getTime().getSplitSecondsInUTCDay());
        final double    delta          = deltaDate + deltaTime.toDouble();
        final double    timeSinceEpoch = (delta - countAtEpoch) / rate;
        return offsetAtEpoch.add(new TimeOffset(drift * timeSinceEpoch)).negate();
    }

    /** Compute date corresponding to some clock count.
     * @param count clock count
     * @return date at {@code count}
     */
    public AbsoluteDate dateAtCount(final double count) {
        return epoch.shiftedBy((count - countAtEpoch) / rate);
    }

    /** Compute clock count corresponding to some date.
     * @param date date
     * @return clock count at {@code date}
     */
    public double countAtDate(final AbsoluteDate date) {
        return countAtEpoch + rate * date.durationFrom(epoch);
    }

    /** {@inheritDoc} */
    @Override
    public <T extends CalculusFieldElement<T>> T offsetFromTAI(final FieldAbsoluteDate<T> date) {
        return date.durationFrom(epoch).multiply(drift).add(offsetAtEpoch.toDouble());
    }

    /** {@inheritDoc} */
    public String getName() {
        return name;
    }

    /** {@inheritDoc} */
    public String toString() {
        return getName();
    }

}