Incorrect AbsoluteDate Returned by Orbit.getPVCoorinates()

Hello. I’m not sure if this is the right place for this post as I am unsure if the issue is user error, expected behaviour, or a bug. Please let me know if this better fits elsewhere.

I’m using Orekit to perform initial orbit determination on a set of observations I have taken. However, while using the Orbit returned by IodGooding, I found that the getPVCoordinates() function was not returning me an instance of TimeStampedPVCoordinates with the same AbsoluteDate as the AbsoluteDate I provided the function. I have been able to replicate the error with made up data in the following code.

// Create three test observations for use in IOD
final double lat = -30;
final double lon = 134;
final double alt = 90.0;
GeodeticPoint stationPoint = new GeodeticPoint(FastMath.toRadians(lat), FastMath.toRadians(lon), alt);
TopocentricFrame stationFrame = new TopocentricFrame(ReferenceEllipsoid.getWgs84(FramesFactory.getITRF(IERSConventions.IERS_2010, true)), stationPoint, "home");
GroundStation groundStation = new GroundStation(stationFrame);
ObservableSatellite satellite = new ObservableSatellite(0);
Frame frame = FramesFactory.getGCRF();

AbsoluteDate date1 =  new AbsoluteDate("2025-12-15T11:11:00.000000000000000000Z", TimeScalesFactory.getUTC());
AbsoluteDate date2 =  new AbsoluteDate("2025-12-15T14:56:00.000000000000000000Z", TimeScalesFactory.getUTC());
AbsoluteDate date3 =  new AbsoluteDate("2025-12-15T18:39:00.000000000000000000Z", TimeScalesFactory.getUTC());

double[] angular1 = {1.5, 0.08};
double[] angular2 = {2.5, 0.04};
double[] angular3 = {3.5, 0.05};

double[] sigma = {FastMath.toRadians(5.0 / 3600.0), FastMath.toRadians(5.0 / 3600.0)};
double[] baseWeight = {1.0, 1.0};

AngularRaDec ob1 = new AngularRaDec(
        groundStation,
        frame,
        date1,
        angular1,
        sigma,
        baseWeight,
        satellite
);
ob1.addModifier(new AberrationModifier());

AngularRaDec ob2 = new AngularRaDec(
        groundStation,
        frame,
        date2,
        angular2,
        sigma,
        baseWeight,
        satellite
);
ob2.addModifier(new AberrationModifier());

AngularRaDec ob3 = new AngularRaDec(
        groundStation,
        frame,
        date3,
        angular3,
        sigma,
        baseWeight,
        satellite
);
ob3.addModifier(new AberrationModifier());


// Perform IOD on observations
NormalizedSphericalHarmonicsProvider gravityField = GravityFieldFactory.getNormalizedProvider(10, 10);
IodGooding iodGooding = new IodGooding(gravityField.getMu());
Orbit orbit = iodGooding.estimate(frame, ob1, ob2, ob3);


// Get coordinates at time of first observation
TimeStampedPVCoordinates pvCoordinates = orbit.getPVCoordinates(date1, frame);

System.out.println("Expected:   " + date1);
System.out.println("Actual:     " + pvCoordinates.getDate());

TimeOffset difference = pvCoordinates.getDate().accurateDurationFrom(date1);
System.out.println("Difference: " + difference.getAttoSeconds() + "e-18s");


// Shift observation date and try again
ob2 = new AngularRaDec(
        groundStation,
        frame,
        date2.shiftedBy(0.123456789),
        angular2,
        sigma,
        baseWeight,
        satellite
);
ob2.addModifier(new AberrationModifier());

orbit = iodGooding.estimate(frame, ob1, ob2, ob3);
pvCoordinates = orbit.getPVCoordinates(date1, frame);

System.out.println("\nExpected:   " + date1);
System.out.println("Actual:     " + pvCoordinates.getDate());

difference = pvCoordinates.getDate().accurateDurationFrom(date1);
System.out.println("Difference: " + difference.getAttoSeconds() + "e-18s");


// Shift observation date and try again
ob2 = new AngularRaDec(
        groundStation,
        frame,
        date2.shiftedBy(100e-18),
        angular2,
        sigma,
        baseWeight,
        satellite
);
ob2.addModifier(new AberrationModifier());

orbit = iodGooding.estimate(frame, ob1, ob2, ob3);
pvCoordinates = orbit.getPVCoordinates(date1, frame);

System.out.println("\nExpected:   " + date1);
System.out.println("Actual:     " + pvCoordinates.getDate());

difference = pvCoordinates.getDate().accurateDurationFrom(date1);
System.out.println("Difference: " + difference.getAttoSeconds() + "e-18s");


// Shift observation date and try again
date1 = date1.shiftedBy(0.5);
ob1 = new AngularRaDec(
        groundStation,
        frame,
        date1,
        angular1,
        sigma,
        baseWeight,
        satellite
);
ob1.addModifier(new AberrationModifier());

ob2 = new AngularRaDec(
        groundStation,
        frame,
        date2.shiftedBy(0.5),
        angular2,
        sigma,
        baseWeight,
        satellite
);
ob2.addModifier(new AberrationModifier());

ob3 = new AngularRaDec(
        groundStation,
        frame,
        date3.shiftedBy(0.5),
        angular3,
        sigma,
        baseWeight,
        satellite
);
ob3.addModifier(new AberrationModifier());

orbit = iodGooding.estimate(frame, ob1, ob2, ob3);
pvCoordinates = orbit.getPVCoordinates(date1, frame);

System.out.println("\nExpected:   " + date1);
System.out.println("Actual:     " + pvCoordinates.getDate());

difference = pvCoordinates.getDate().accurateDurationFrom(date1);
System.out.println("Difference: " + difference.getAttoSeconds() + "e-18s");

This code produces the following output:

Expected: 2025-12-15T11:11:00.000Z
Actual: 2025-12-15T11:11:00.000Z
Difference: 0e-18s

Expected: 2025-12-15T11:11:00.000Z
Actual: 2025-12-15T11:10:59.999999999999443552Z
Difference: 999999999999443552e-18s

Expected: 2025-12-15T11:11:00.000Z
Actual: 2025-12-15T11:11:00.0000000000000001Z
Difference: 100e-18s

Expected: 2025-12-15T11:11:00.500Z
Actual: 2025-12-15T11:11:00.500Z
Difference: 0e-18s

In attempting to replicate the behaviour, I found that the issue didn’t occur when the dates I provided had the same fractional second part (see above). The real observations used had fractional second parts .961481000000000384s, .435758000000000384s, and .852012000000000384s (the 384 at the end appears after processing my observations with Orekit) with a difference from the expected date of 652992e-18s.

I am using Orekit version 13.1.2 and OpenJDK 16.

Since I was attempting to validate AbsoluteDates at several points in my pipeline, this behaviour was causing issues, as the dates returned were not as expected. I was able to rewrite my code to avoid using getPVCoordinates(), but I still thought it worth mentioning somewhere that I came across this issue. Thanks.

Hello @nfxby

I imagine someone from the development team can give you a more accurate answer, since I know that some extra steps are taken when dealing with AbsoluteDate to try to avoid floating point errors and stuff like that.

However, I would just like to point out that the differences you’re seeing are all below the machine epsilon for double precision values (~1.1e-16).
Basically, after the 16th decimal point, two apparently different numbers should be treated as the same value, since there is no actual guarantee that the digits shown are correct.
This is one of the main reason why it’s generally not recommended to make assertions on the equality of two non-integer numbers, but rather check that the difference is in the same order of magnitude of the machine epsilon (or thereabouts).
A classic example of this is this one (in python):

1 Like

Hello @nfxby and welcome to the orekit forum !

First of all, thank you for providing a simple test because this helps a lot :+1:

As pointed out by @Emiliano, i would not recommend this way of testing because your tests will likely not be robust as this may be below machine epsilon precision.

Luc introduced atto seconds precision not so long ago so i’m not much aware of what is to be expected but i do find strange that

does not reflect the real difference of -5.56448E-13 that we find with the durationFrom() method.

Regarding your initial question, i would suggest you to use some tolerance threshold that you consider consistent with how you are going to use it afterwards.

Feel free to ask questions you may have !

Cheers,
Vincent

Thank you both for the replies. Yes, if I set a tolerance high enough, the date it gives me is “equal” in the sense that the error is within the expected bounds. The “issue” that I am bringing up with my post, I guess, is that, morally, I believe if I ask getPVCoordinates() for PVCoordinates at AbsoluteDate X, it should return an instance of TimeStampedPVCoordinates with AbsoluteDate X. This is not the case. I would like to point out that the error I see with my real data is not below machine epsilon (order of 1e-13 vs machine epsilon of 1e-16).

Why can it return a date other than what I gave it? If the difference between the returned and given date is always insigificant, why doesn’t it just set the returned date to the given date? How can I be confident that the difference is always insignificant considering the error can be greater than machine epsilon? And, finally, if the difference can be significant, are the coordinates given to me at the date attached to them or at the date I requested in the first place?

Cheers.

Indeed it is disturbing to see that the requested date is not exactly the one we get.

I managed to locate the “issue” precisely and wrote the following test to demonstrate it:

import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeOffset;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScalesFactory;

import static org.example.Utils.loadOrekitData;

public class DatePrecisionIssueSimple {
   public static void main(String[] args) {
      // Load orekit data
      loadOrekitData();

      // Define dates
      final TimeScale utc          = TimeScalesFactory.getUTC();
      AbsoluteDate    date1        = new AbsoluteDate("2025-12-15T11:11:00.000000000000000000Z", utc);
      AbsoluteDate    date2        = new AbsoluteDate("2025-12-15T14:56:00.000000000000000000Z", utc);
      AbsoluteDate    date2Shifted = date2.shiftedBy(0.123456789);

      // Compute delta
      final double     deltaDouble     = date1.durationFrom(date2Shifted);
      final TimeOffset deltaTimeOffset = date1.accurateDurationFrom(date2Shifted);

      // Shift delta 2 shifted back to date 1
      final AbsoluteDate date2BackToDate1Double     = date2Shifted.shiftedBy(deltaDouble);
      final AbsoluteDate date2BackToDate1TimeOffset = date2Shifted.shiftedBy(deltaTimeOffset);

      // print difference
      printDateDifference(date1, date2BackToDate1Double);
      printDateDifference(date1, date2BackToDate1TimeOffset);
   }

   private static void printDateDifference(AbsoluteDate date, AbsoluteDate dateShifted) {
      System.out.println("Date: " + date.getDate());
      System.out.println("Shifted date: " + dateShifted.getDate());
      System.out.println("Difference : " + dateShifted.accurateDurationFrom(date).getAttoSeconds() + " * 1e-18 s");
      System.out.println();
   }
}

The output :

Date: 2025-12-15T11:11:00.000Z
Shifted date: 2025-12-15T11:10:59.999999999999443552Z
Difference : 999999999999443552 * 1e-18 s

Date: 2025-12-15T11:11:00.000Z
Shifted date: 2025-12-15T11:11:00.000Z
Difference : 0 * 1e-18 s

When using a primitive double to get the delta between the two dates, we are losing some precision during our way back to date 1. However, when using the TimeOffset which aims to increase precision, we do find our way back to the exact date.

This could be considered an issue and we would have to fix the getPVCoordinates methods that currently use the getDurationFrom method instead of the getAccurateDurationFrom. This would however introduce additional overhead on a very low level component so i guess it is up to debate ? @orekit_dev_team

In any case, thank you for pointing this out !

Cheers,
Vincent

I agree with this explanation.
The culprit seems to be at line 136 of ShiftablePVCoordinatesHolder.java (and probably also in the field version).

No, the opposite is true. In fact durationFrom calls first accurateDurationFrom and then converts it to a double, which is converted back when shiftedBy is called.

1 Like

Thanks for the analysis Vincent and Luc.
I guess if there is a faster and more accurate way of doing things, it qualifies for a patch I would say. Thoughts?

Cheers,
Romain.

I agree ! I’ll have some time tomorrow morning if it’s fine with you @Serrof ?

UPDATE: Issue created Fix ShiftablePVCoordinatesHolder getPVCoordinates for date issue (#1883) · Issues · Orekit / Orekit · GitLab

1 Like

Hi all,

So Vincent kindly started a fix and we’ve discussed about it. The thing it that to apply the same thing to Field stuff, it needs more work (like the introduction of a FieldTimeOffset). And also it’s probably not just PVCoordinates that is affected, but all inheritors of Shiftable. So I’m starting to think, should we just include all this in 14.0 and not 13.1.3? I don’t think it’s a critical bug. And this way we can have the patch release sooner.
I’m leaving it up for debate here.

Cheers,
Romain.

1 Like

Hi all,

In light of this, should we have an combined interface for Timestamped and Timeshiftable. The point would be to have a shiftedTo method on top of shiftedBy.
That would be for orekit 14.0.

Cheers,
Romain.