IodHerrickGibbs.java

/* Copyright 2022-2025 Bryan Cazabonne
 * 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.
 * Bryan Cazabonne 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.estimation.iod;

import org.hipparchus.CalculusFieldElement;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.estimation.measurements.PV;
import org.orekit.estimation.measurements.Position;
import org.orekit.frames.Frame;
import org.orekit.orbits.CartesianOrbit;
import org.orekit.orbits.FieldCartesianOrbit;
import org.orekit.orbits.FieldOrbit;
import org.orekit.orbits.Orbit;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.FieldAbsoluteDate;
import org.orekit.utils.FieldPVCoordinates;
import org.orekit.utils.PVCoordinates;

/**
 * HerrickGibbs position-based Initial Orbit Determination (IOD) algorithm.
 * <p>
 * An orbit is determined from three position vectors. Because Gibbs IOD algorithm
 * is limited when the position vectors are to close to one other, Herrick-Gibbs
 * IOD algorithm is a variation made to address this limitation.
 * Because this method is only approximate, it is not robust as the Gibbs method
 * for other cases.
 * </p>
 *
 * @see "Vallado, D., Fundamentals of Astrodynamics and Applications, 4th Edition."
 *
 * @author Bryan Cazabonne
 * @since 13.1
 */
public class IodHerrickGibbs {

    /** Gravitational constant. **/
    private final double mu;

    /** Threshold for checking coplanar vectors (Ref: Equation 7-27). */
    private final double COPLANAR_THRESHOLD = FastMath.toRadians(3.);

    /** Constructor.
     * @param mu gravitational constant
     */
    public IodHerrickGibbs(final double mu) {
        this.mu = mu;
    }

    /** Give an initial orbit estimation, assuming Keplerian motion.
     * <p>
     * All observations should be from the same location.
     * </p>
     * @param frame measurements frame, used as output orbit frame
     * @param p1 First position measurement
     * @param p2 Second position measurement
     * @param p3 Third position measurement
     * @return an initial orbit estimation at the central date
     *         (i.e., date of the second position measurement)
     */
    public Orbit estimate(final Frame frame, final Position p1, final Position p2, final Position p3) {
        return estimate(frame, p1.getPosition(), p1.getDate(),
                        p2.getPosition(), p2.getDate(),
                        p3.getPosition(), p3.getDate());
    }

    /** Give an initial orbit estimation, assuming Keplerian motion.
     * <p>
     * All observations should be from the same location.
     * </p>
     * @param frame measurements frame, used as output orbit frame
     * @param pv1 First PV measurement
     * @param pv2 Second PV measurement
     * @param pv3 Third PV measurement
     * @return an initial orbit estimation at the central date
     *         (i.e., date of the second PV measurement)
     */
    public Orbit estimate(final Frame frame, final PV pv1, final PV pv2, final PV pv3) {
        return estimate(frame, pv1.getPosition(), pv1.getDate(),
                        pv2.getPosition(), pv2.getDate(),
                        pv3.getPosition(), pv3.getDate());
    }

    /** Give an initial orbit estimation, assuming Keplerian motion.
     * <p>
     * All observations should be from the same location.
     * </p>
     * @param frame measurements frame, used as output orbit frame
     * @param r1 position vector 1, expressed in frame
     * @param date1 epoch of position vector 1
     * @param r2 position vector 2, expressed in frame
     * @param date2 epoch of position vector 2
     * @param r3 position vector 3, expressed in frame
     * @param date3 epoch of position vector 3
     * @return an initial orbit estimation at the central date
     *         (i.e., date of the second position measurement)
     */
    public Orbit estimate(final Frame frame,
                          final Vector3D r1, final AbsoluteDate date1,
                          final Vector3D r2, final AbsoluteDate date2,
                          final Vector3D r3, final AbsoluteDate date3) {

        // Verify that measurements are not at the same date
        verifyMeasurementEpochs(date1, date2, date3);

        // Get the difference of time between the position vectors
        final double tau32 = date3.getDate().durationFrom(date2.getDate());
        final double tau31 = date3.getDate().durationFrom(date1.getDate());
        final double tau21 = date2.getDate().durationFrom(date1.getDate());

        // Check that measurements are in the same plane
        final double num = r1.normalize().dotProduct(r2.normalize().crossProduct(r3.normalize()));
        if (FastMath.abs(FastMath.PI / 2.0 - FastMath.acos(num)) > COPLANAR_THRESHOLD) {
            throw new OrekitException(OrekitMessages.NON_COPLANAR_POINTS);
        }

        // Compute velocity vector
        final double muOTwelve = mu / 12.0;
        final double coefficient1 = -tau32 * (1.0 / (tau21 * tau31) + muOTwelve / pow3(r1.getNorm()));
        final double coefficient2 = (tau32 - tau21) * (1.0 / (tau21 * tau32) + muOTwelve / pow3(r2.getNorm()));
        final double coefficient3 = tau21 * (1.0 / (tau32 * tau31) + muOTwelve / pow3(r3.getNorm()));
        final Vector3D v2 = r1.scalarMultiply(coefficient1).add(r2.scalarMultiply(coefficient2)).add(r3.scalarMultiply(coefficient3));

        // Convert to an orbit
        return new CartesianOrbit( new PVCoordinates(r2, v2), frame, date2, mu);
    }

    /** Give an initial orbit estimation, assuming Keplerian motion.
     * <p>
     * All observations should be from the same location.
     * </p>
     * @param frame measurements frame, used as output orbit frame
     * @param r1 position vector 1, expressed in frame
     * @param date1 epoch of position vector 1
     * @param r2 position vector 2, expressed in frame
     * @param date2 epoch of position vector 2
     * @param r3 position vector 3, expressed in frame
     * @param date3 epoch of position vector 3
     * @param <T> type of the elements
     * @return an initial orbit estimation at the central date
     *         (i.e., date of the second position measurement)
     */
    public <T extends CalculusFieldElement<T>> FieldOrbit<T> estimate(final Frame frame,
                                                                      final FieldVector3D<T> r1, final FieldAbsoluteDate<T> date1,
                                                                      final FieldVector3D<T> r2, final FieldAbsoluteDate<T> date2,
                                                                      final FieldVector3D<T> r3, final FieldAbsoluteDate<T> date3) {

        // Verify that measurements are not at the same date
        verifyMeasurementEpochs(date1.toAbsoluteDate(), date2.toAbsoluteDate(), date3.toAbsoluteDate());

        // Get the difference of time between the position vectors
        final T tau32 = date3.getDate().durationFrom(date2.getDate());
        final T tau31 = date3.getDate().durationFrom(date1.getDate());
        final T tau21 = date2.getDate().durationFrom(date1.getDate());

        // Check that measurements are in the same plane
        final T num = r1.normalize().dotProduct(r2.normalize().crossProduct(r3.normalize()));
        if (FastMath.abs(FastMath.PI / 2.0 - FastMath.acos(num.getReal())) > COPLANAR_THRESHOLD) {
            throw new OrekitException(OrekitMessages.NON_COPLANAR_POINTS);
        }

        // Compute velocity vector
        final double muOTwelve = mu / 12.0;
        final T coefficient1 = tau32.negate().multiply(tau21.multiply(tau31).reciprocal().add(pow3(r1.getNorm()).reciprocal().multiply(muOTwelve)));
        final T coefficient2 = tau32.subtract(tau21).multiply(tau21.multiply(tau32).reciprocal().add(pow3(r2.getNorm()).reciprocal().multiply(muOTwelve)));
        final T coefficient3 = tau21.multiply(tau32.multiply(tau31).reciprocal().add(pow3(r3.getNorm()).reciprocal().multiply(muOTwelve)));
        final FieldVector3D<T> v2 = r1.scalarMultiply(coefficient1).add(r2.scalarMultiply(coefficient2)).add(r3.scalarMultiply(coefficient3));

        // Convert to an orbit
        return new FieldCartesianOrbit<>( new FieldPVCoordinates<>(r2, v2), frame, date2, date1.getField().getZero().add(mu));
    }

    /** Compute the cubic value.
     * @param value value
     * @return value^3
     */
    private static double pow3(final double value) {
        return value * value * value;
    }

    /** Compute the cubic value.
     * @param value value
     * @param <T> type of the elements
     * @return value^3
     */
    private static <T extends CalculusFieldElement<T>> T pow3(final T value) {
        return value.multiply(value).multiply(value);
    }

    /** Verifies that measurements are not at the same date.
     * @param date1 epoch of position vector 1
     * @param date2 epoch of position vector 2
     * @param date3 epoch of position vector 3
     */
    private void verifyMeasurementEpochs(final AbsoluteDate date1, final AbsoluteDate date2, final AbsoluteDate date3) {
        if (date1.equals(date2) || date1.equals(date3) || date2.equals(date3)) {
            throw new OrekitException(OrekitMessages.NON_DIFFERENT_DATES_FOR_OBSERVATIONS, date1, date2, date3,
                    date2.durationFrom(date1), date3.durationFrom(date1), date3.durationFrom(date2));
        }
    }
}