OrbitHermiteInterpolator.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.orbits;
import org.hipparchus.analysis.interpolation.HermiteInterpolator;
import org.hipparchus.util.MathUtils;
import org.orekit.errors.OrekitInternalError;
import org.orekit.frames.Frame;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeInterpolator;
import org.orekit.utils.CartesianDerivativesFilter;
import org.orekit.utils.TimeStampedPVCoordinates;
import org.orekit.utils.TimeStampedPVCoordinatesHermiteInterpolator;
import java.util.List;
import java.util.stream.Stream;
/**
* Class using a Hermite interpolator to interpolate orbits.
* <p>
* Depending on given sample orbit type, the interpolation may differ :
* <ul>
* <li>For Keplerian, Circular and Equinoctial orbits, the interpolated instance is created by polynomial Hermite
* interpolation, using derivatives when available. </li>
* <li>For Cartesian orbits, the interpolated instance is created using the cartesian derivatives filter given at
* instance construction. Hence, it will fall back to Lagrange interpolation if this instance has been designed to not
* use derivatives.
* </ul>
* <p>
* In any case, it should be used only with small number of interpolation points (about 10-20 points) in order to avoid
* <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's phenomenon</a> and numerical problems
* (including NaN appearing).
*
* @author Luc Maisonobe
* @author Vincent Cucchietti
* @see Orbit
* @see HermiteInterpolator
*/
public class OrbitHermiteInterpolator extends AbstractOrbitInterpolator {
/** Filter for derivatives from the sample to use in position-velocity-acceleration interpolation. */
private final CartesianDerivativesFilter pvaFilter;
/**
* Constructor with :
* <ul>
* <li>Default number of interpolation points of {@code DEFAULT_INTERPOLATION_POINTS}</li>
* <li>Default extrapolation threshold value ({@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s)</li>
* <li>Use of position and two time derivatives during interpolation</li>
* </ul>
* As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
* points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
* phenomenon</a> and numerical problems (including NaN appearing).
*
* @param outputInertialFrame output inertial frame
*/
public OrbitHermiteInterpolator(final Frame outputInertialFrame) {
this(DEFAULT_INTERPOLATION_POINTS, outputInertialFrame);
}
/**
* Constructor with :
* <ul>
* <li>Default extrapolation threshold value ({@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s)</li>
* <li>Use of position and two time derivatives during interpolation</li>
* </ul>
* As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
* points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
* phenomenon</a> and numerical problems (including NaN appearing).
*
* @param interpolationPoints number of interpolation points
* @param outputInertialFrame output inertial frame
*/
public OrbitHermiteInterpolator(final int interpolationPoints, final Frame outputInertialFrame) {
this(interpolationPoints, outputInertialFrame, CartesianDerivativesFilter.USE_PVA);
}
/**
* Constructor with default extrapolation threshold value ({@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s).
* <p>
* As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
* points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
* phenomenon</a> and numerical problems (including NaN appearing).
*
* @param interpolationPoints number of interpolation points
* @param outputInertialFrame output inertial frame
* @param pvaFilter filter for derivatives from the sample to use in position-velocity-acceleration interpolation. Used
* only when interpolating Cartesian orbits.
*/
public OrbitHermiteInterpolator(final int interpolationPoints, final Frame outputInertialFrame,
final CartesianDerivativesFilter pvaFilter) {
this(interpolationPoints, DEFAULT_EXTRAPOLATION_THRESHOLD_SEC, outputInertialFrame, pvaFilter);
}
/**
* Constructor.
* <p>
* As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
* points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
* phenomenon</a> and numerical problems (including NaN appearing).
*
* @param interpolationPoints number of interpolation points
* @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
* @param outputInertialFrame output inertial frame
* @param pvaFilter filter for derivatives from the sample to use in position-velocity-acceleration interpolation. Used
* only when interpolating Cartesian orbits.
*/
public OrbitHermiteInterpolator(final int interpolationPoints, final double extrapolationThreshold,
final Frame outputInertialFrame, final CartesianDerivativesFilter pvaFilter) {
super(interpolationPoints, extrapolationThreshold, outputInertialFrame);
this.pvaFilter = pvaFilter;
}
/** Get filter for derivatives from the sample to use in position-velocity-acceleration interpolation.
* @return filter for derivatives from the sample to use in position-velocity-acceleration interpolation
*/
public CartesianDerivativesFilter getPVAFilter() {
return pvaFilter;
}
/**
* {@inheritDoc}
* <p>
* Depending on given sample orbit type, the interpolation may differ :
* <ul>
* <li>For Keplerian, Circular and Equinoctial orbits, the interpolated instance is created by polynomial Hermite
* interpolation, using derivatives when available. </li>
* <li>For Cartesian orbits, the interpolated instance is created using the cartesian derivatives filter given at
* instance construction. Hence, it will fall back to Lagrange interpolation if this instance has been designed to not
* use derivatives.
* </ul>
* If orbit interpolation on large samples is needed, using the {@link
* org.orekit.propagation.analytical.Ephemeris} class is a better way than using this
* low-level interpolation. The Ephemeris class automatically handles selection of
* a neighboring sub-sample with a predefined number of point from a large global sample
* in a thread-safe way.
*
* @param interpolationData interpolation data
*
* @return interpolated instance at given date
*/
@Override
protected Orbit interpolate(final InterpolationData interpolationData) {
// Get orbit sample
final List<Orbit> sample = interpolationData.getNeighborList();
// Get orbit type for interpolation
final OrbitType orbitType = sample.get(0).getType();
if (orbitType == OrbitType.CARTESIAN) {
return interpolateCartesian(interpolationData.getInterpolationDate(), sample);
}
else {
return interpolateCommon(interpolationData.getInterpolationDate(), sample, orbitType);
}
}
/**
* Interpolate Cartesian orbit using specific method for Cartesian orbit.
*
* @param interpolationDate interpolation date
* @param sample orbits sample
*
* @return interpolated Cartesian orbit
*/
private CartesianOrbit interpolateCartesian(final AbsoluteDate interpolationDate, final List<Orbit> sample) {
// Create time stamped position-velocity-acceleration Hermite interpolator
final TimeInterpolator<TimeStampedPVCoordinates> interpolator =
new TimeStampedPVCoordinatesHermiteInterpolator(getNbInterpolationPoints(), getExtrapolationThreshold(),
pvaFilter);
// Convert sample to stream
final Stream<Orbit> sampleStream = sample.stream();
// Map time stamped position-velocity-acceleration coordinates
final Stream<TimeStampedPVCoordinates> sampleTimeStampedPV = sampleStream.map(Orbit::getPVCoordinates);
// Interpolate PVA
final TimeStampedPVCoordinates interpolated = interpolator.interpolate(interpolationDate, sampleTimeStampedPV);
// Use first entry gravitational parameter
final double mu = sample.get(0).getMu();
return new CartesianOrbit(interpolated, getOutputInertialFrame(), interpolationDate, mu);
}
/**
* Method gathering common parts of interpolation between circular, equinoctial and keplerian orbit.
*
* @param interpolationDate interpolation date
* @param orbits orbits sample
* @param orbitType interpolation method to use
*
* @return interpolated orbit
*/
private Orbit interpolateCommon(final AbsoluteDate interpolationDate, final List<Orbit> orbits,
final OrbitType orbitType) {
// Use first entry gravitational parameter
final double mu = orbits.get(0).getMu();
// Interpolate and build a new instance
final double[][] interpolated;
switch (orbitType) {
case CIRCULAR:
interpolated = interpolateCircular(interpolationDate, orbits);
return new CircularOrbit(interpolated[0][0], interpolated[0][1], interpolated[0][2],
interpolated[0][3], interpolated[0][4], interpolated[0][5],
interpolated[1][0], interpolated[1][1], interpolated[1][2],
interpolated[1][3], interpolated[1][4], interpolated[1][5],
PositionAngleType.MEAN, getOutputInertialFrame(), interpolationDate, mu);
case KEPLERIAN:
interpolated = interpolateKeplerian(interpolationDate, orbits);
return new KeplerianOrbit(interpolated[0][0], interpolated[0][1], interpolated[0][2],
interpolated[0][3], interpolated[0][4], interpolated[0][5],
interpolated[1][0], interpolated[1][1], interpolated[1][2],
interpolated[1][3], interpolated[1][4], interpolated[1][5],
PositionAngleType.MEAN, getOutputInertialFrame(), interpolationDate, mu);
case EQUINOCTIAL:
interpolated = interpolateEquinoctial(interpolationDate, orbits);
return new EquinoctialOrbit(interpolated[0][0], interpolated[0][1], interpolated[0][2],
interpolated[0][3], interpolated[0][4], interpolated[0][5],
interpolated[1][0], interpolated[1][1], interpolated[1][2],
interpolated[1][3], interpolated[1][4], interpolated[1][5],
PositionAngleType.MEAN, getOutputInertialFrame(), interpolationDate, mu);
default:
// Should never happen
throw new OrekitInternalError(null);
}
}
/**
* Build interpolating functions for circular orbit parameters.
*
* @param interpolationDate interpolation date
* @param orbits orbits sample
*
* @return interpolating functions for circular orbit parameters
*/
private double[][] interpolateCircular(final AbsoluteDate interpolationDate, final List<Orbit> orbits) {
// Set up an interpolator
final HermiteInterpolator interpolator = new HermiteInterpolator();
// Second pass to feed interpolator
AbsoluteDate previousDate = null;
double previousRAAN = Double.NaN;
double previousAlphaM = Double.NaN;
for (final Orbit orbit : orbits) {
final CircularOrbit circ = (CircularOrbit) OrbitType.CIRCULAR.convertType(orbit);
final double continuousRAAN;
final double continuousAlphaM;
if (previousDate == null) {
continuousRAAN = circ.getRightAscensionOfAscendingNode();
continuousAlphaM = circ.getAlphaM();
}
else {
final double dt = circ.getDate().durationFrom(previousDate);
final double keplerAM = previousAlphaM + circ.getKeplerianMeanMotion() * dt;
continuousRAAN = MathUtils.normalizeAngle(circ.getRightAscensionOfAscendingNode(), previousRAAN);
continuousAlphaM = MathUtils.normalizeAngle(circ.getAlphaM(), keplerAM);
}
previousDate = circ.getDate();
previousRAAN = continuousRAAN;
previousAlphaM = continuousAlphaM;
interpolator.addSamplePoint(circ.getDate().durationFrom(interpolationDate),
new double[] { circ.getA(),
circ.getCircularEx(),
circ.getCircularEy(),
circ.getI(),
continuousRAAN,
continuousAlphaM },
new double[] { circ.getADot(),
circ.getCircularExDot(),
circ.getCircularEyDot(),
circ.getIDot(),
circ.getRightAscensionOfAscendingNodeDot(),
circ.getAlphaMDot() });
}
return interpolator.derivatives(0.0, 1);
}
/**
* Build interpolating functions for keplerian orbit parameters.
*
* @param interpolationDate interpolation date
* @param orbits orbits sample
*
* @return interpolating functions for keplerian orbit parameters
*/
private double[][] interpolateKeplerian(final AbsoluteDate interpolationDate, final List<Orbit> orbits) {
// Set up an interpolator
final HermiteInterpolator interpolator = new HermiteInterpolator();
// Second pass to feed interpolator
AbsoluteDate previousDate = null;
double previousPA = Double.NaN;
double previousRAAN = Double.NaN;
double previousM = Double.NaN;
for (final Orbit orbit : orbits) {
final KeplerianOrbit kep = (KeplerianOrbit) OrbitType.KEPLERIAN.convertType(orbit);
final double continuousPA;
final double continuousRAAN;
final double continuousM;
if (previousDate == null) {
continuousPA = kep.getPerigeeArgument();
continuousRAAN = kep.getRightAscensionOfAscendingNode();
continuousM = kep.getMeanAnomaly();
}
else {
final double dt = kep.getDate().durationFrom(previousDate);
final double keplerM = previousM + kep.getKeplerianMeanMotion() * dt;
continuousPA = MathUtils.normalizeAngle(kep.getPerigeeArgument(), previousPA);
continuousRAAN = MathUtils.normalizeAngle(kep.getRightAscensionOfAscendingNode(), previousRAAN);
continuousM = MathUtils.normalizeAngle(kep.getMeanAnomaly(), keplerM);
}
previousDate = kep.getDate();
previousPA = continuousPA;
previousRAAN = continuousRAAN;
previousM = continuousM;
interpolator.addSamplePoint(kep.getDate().durationFrom(interpolationDate),
new double[] { kep.getA(),
kep.getE(),
kep.getI(),
continuousPA,
continuousRAAN,
continuousM },
new double[] { kep.getADot(),
kep.getEDot(),
kep.getIDot(),
kep.getPerigeeArgumentDot(),
kep.getRightAscensionOfAscendingNodeDot(),
kep.getMeanAnomalyDot() });
}
return interpolator.derivatives(0.0, 1);
}
/**
* Build interpolating functions for equinoctial orbit parameters.
*
* @param interpolationDate interpolation date
* @param orbits orbits sample
*
* @return interpolating functions for equinoctial orbit parameters
*/
private double[][] interpolateEquinoctial(final AbsoluteDate interpolationDate, final List<Orbit> orbits) {
// Set up an interpolator
final HermiteInterpolator interpolator = new HermiteInterpolator();
// Second pass to feed interpolator
AbsoluteDate previousDate = null;
double previousLm = Double.NaN;
for (final Orbit orbit : orbits) {
final EquinoctialOrbit equi = (EquinoctialOrbit) OrbitType.EQUINOCTIAL.convertType(orbit);
final double continuousLm;
if (previousDate == null) {
continuousLm = equi.getLM();
}
else {
final double dt = equi.getDate().durationFrom(previousDate);
final double keplerLm = previousLm + equi.getKeplerianMeanMotion() * dt;
continuousLm = MathUtils.normalizeAngle(equi.getLM(), keplerLm);
}
previousDate = equi.getDate();
previousLm = continuousLm;
interpolator.addSamplePoint(equi.getDate().durationFrom(interpolationDate),
new double[] { equi.getA(),
equi.getEquinoctialEx(),
equi.getEquinoctialEy(),
equi.getHx(),
equi.getHy(),
continuousLm },
new double[] {
equi.getADot(),
equi.getEquinoctialExDot(),
equi.getEquinoctialEyDot(),
equi.getHxDot(),
equi.getHyDot(),
equi.getLMDot() });
}
return interpolator.derivatives(0.0, 1);
}
}