Help understanding correct time conversion

Hello, I am brand-new to Orekit and am trying to convert among Calendar, GPS, JDD, MJD and ISO 8601 formats, and I’m unsure of the canonical way to do this. In the case of GPS, I see on the forum that it’s suggested to use the GNSSDate class. So I did the following (using the Python wrapper, though I don’t think this makes a difference):

utcDateFrom8601 = AbsoluteDate(“2023-11-17T10:01:42.200Z”, TimeScalesFactory.getUTC())
gnssDateFromUTC = GNSSDate(utcDateFrom8601, SatelliteSystem.GPS)

print(gnssDateFromUTC.getDate()) → 2023-11-17T10:01:42.200Z

This appears to be the internal AbsoluteDate object. Had I used TimeScalesFactory.getGPS() instead, it would be 18 leap seconds less. My understanding seemed OK to this point.

Then I get the week number and time of week:

print(gnssDateFromUTC.getWeekNumber()) → 2288
print(gnssDateFromUTC.getMilliInWeek()) → 468120200.0

and construct a new GNSSDate object using this information:

gnssDateFromWeekMilli = GNSSDate(2288, 468120200.0, SatelliteSystem.GPS)

However, the AbsoluteDate value of GNSSDate built from the UTC doesn’t exactly equal that of the GNSSDate built from its week and milli values:

print(gnssDateFromWeekMilli.getDate()) → 2023-11-17T10:01:42.20000000001164Z

Now, a difference of .00000000001164, being a fraction of a nanosecond, doesn’t seem meaningful between GPS and UTC according to any resolution guarantees, but .isEqualTo is false, and in general I suspect I must not be going about conversions the right way, or perhaps it’s understood that precision needs to be limited in some cases, etc. If someone can help me understand this or point me to canonical example code for handling such conversions accurately, it would be greatly appreciated.

I guess you loose some accuracy here because there are many milliseconds in one week (about 604 millions), and the second argument when building GNSSDate is a IEEE754 binary64 number, which has slightly less than 16 decimal digits accuracy (really 53 bits due to the 52 bits mantissa and implicit 1 convention for normal numbers) and that is expected to be seconds, not milliseconds. So near the end of the week the ulp (Unit in Last Position) of this argument could reach about 604e6 * 1e-16 = 604e-10 ms or 60.4 ns, and this number is not represented exactly in IEEE754 due to the division by 1000 (1.0/1000 is not represented exactly in IEEE754 binary64 format).

When using AbsoluteDate, we take great care about the accuracy, and we store the whole seconds as a long and the fractional part in a double that remains between 0s (included) and 1s (excluded), so we keep an accuracy at femtosecond level. Using large milliseconds in week generates inaccuracy.

One way to work around that is to avoid these large numbers before we divide the milliseconds by 1000 to get second (which is what we store). So in your case, when you have 468120200.0 milliseconds (which can be represented exactly in IEEE754 since it really is an integer represented as a double), then you could split it as 468120 seconds + 200 milliseconds. You may then create a GNSSDate from the week plus 468120s, extract the AbsoluteDate, use shiftedBy to move it 200ms later and rebuild a GNSSDate from that. Yes, it is cumbersome but if you want real accuracy, you need this. Big milliseconds counts is really a problem.

Just adding something, as there was a change between versions 11.X and 12.0 published recently.

Up to 11.X, the double argument did represent milliseconds and the division by 1000 was done internally. Since 12.0, the double argument now represents seconds and the division by 1000 must be done by caller. This was done as part of solving issue 1013, according to this forum thread, as in fact the number of milliseconds only appears in the RF signal (where it is an integer).

Regardless of where the division by 1000 is done, internally by GNSSDate up to version 11.X or externally by the caller starting in version 12.0, the loss of accuracy occurs when the division by 1000 is done. So basically in Orekit 12.0, the accuracy will already be lost right before the GNSSDate constructor is called (really when the second constructor argument is built), unless you take care to separate whole seconds for sub-second before performing the division as explained in the previous message.

Thank you very much for your detailed replies! I was concerned as to whether I was approaching conversions correctly in Orekit. I understand perfectly well that time handling is a very complex issue on any platform, as are the inherent limitations of number representation. I am using 11.X but will look at version 12 and consider the options and tradeoffs.