Number of decimal digits in OEM file

Hello everyone,
I’d like to know if it is possible to force 3 decimal digits for seconds in the date/time record when writing OEM files. I.e. I’d like to force format:
yyyy-mm-ddTHH:MM:SS.sss (3 decimal digits for seconds)
But the default configuration seems to produce format:
yyyy-mm-ddTHH:MM:SS.s (1 decimal digit for seconds)
And this last format is rejected by space-track.org
A workaround to produce always 3 decimal digits would be appreciated.
Thank you.
Kind Regards,
Alfredo

The number of decimals in all CCSDS output data depends on the input.
We use Ulf Adam’s Ryū algorithm for this, which generates “the shortest decimal representation of a floating point number that maintains round-trip safety. That is, a correct parser can recover the exact original number.”

This choice was made to avoid truncation problems as CCSDS is a text-based format. I have encountered several times in my carrier accuracy problems linked to intermediate files with too few digits in entries. The worst case is when attempting to recover derivatives using polynomial fitting across several entries in an ephemeris: a lot of numerical noise is introduced by the truncation.

The point is “shortest decimal representation” may be very short if the number to be represented happens to be close to a decimal number with a small number of digits.

CCSDS does not mandates any number of digits for data, regardless it is a date or something else. In the case of dates, the standard reads:

In value fields that represent an absolute time tag or epoch, times shall
be given in one of the following two formats:
YYYY-MM-DDThh:mm:ss[.d→d][Z]
or
YYYY-DDDThh:mm:ss[.d→d][Z],
where ‘YYYY’ is the year; ‘MM’ is the two-digit month; ‘DD’ is the two-digit
day; ‘DDD’ is the three-digit day of year; ‘T’ is constant; ‘hh:mm:ss[.d→d]’
is the time in hours, minutes, seconds, and optional fractional seconds;
and ‘Z’ is an optional time code terminator (the only permitted value is ‘Z’
for Zulu, i.e., UTC). As many ‘d’ characters to the right of the period as
required may be used to obtain the required precision, up to the maximum
allowed for a fixed-point number. All fields shall have leading zeros. (See
reference [2], ASCII Time Code A or B.)
NOTE – During a leap second introduction, the value of the two-digit integer
seconds (ss) field shall be ‘60’ as specified in reference [2].

So the dates generated by OemWriter are fully compliant with CCSDS standard. We do however truncate one date to whole seconds only: the mandatory CREATION_DATE entry in the header (and there is probably no good reason for this truncation here).

However, there is a workaround because all dates (except CREATION_DATE) are formatted by a single method in the Generator used (which is either KvnGenerator or XmlGenerator). So you can override this method when building the generator. Here is how to do that for KVN, but the exact same feature can be used for XML:

// override dateToString to enforce 3 decimal digits exactly
// beware it is less accurate than the default behavior
// that uses Ryū algorithm
Generator generator = new KvnGenerator(output, paddingWidth,
                                       outputName, unitsColumn) {
            /** {@inheritDoc} */
            @Override
            public String dateToString(int year, int month, int day,
                                       int hour, int minute, double seconds) {
                return String.format(Locale.US,
                                     "%04d-%02d-%02dT%02d:%02d:%6.3f",
                                     year, month, day, hour, minute, seconds);
            }
        }

You can then use the generator as before when building your OEM writer.

I guess you should ask space-track to allow any number of digits in dates as per CCSDS standard.

1 Like

Dear Luc,
this is very helpful to me.
I’m not sure how to implement it and where.
This is my Python code:

        # OrekitEphemerisFile
        self.ephemerisFile = OrekitEphemerisFile()
        self.OrekitEphemerisSatellite = self.ephemerisFile.addSatellite(self.objId)
        self.OrekitEphemerisSatellite.addNewSegment(self.java_state_list)
        oemFile = File(filepath)
        output = PrintStream(oemFile)
        # Header
        oem_header = Header(CCSDS_VERSION)
        oem_header.setFormatVersion(CCSDS_VERSION)
        oem_header.setCreationDate(AbsoluteDate(datetime.utcnow().isoformat(), UTC))
        oem_header.setOriginator(ORIGINATOR)
        # template
        template = OemMetadata(2)
        template.setTimeSystem(TimeSystem.UTC)
        template.setObjectID(self.objId)
        template.setObjectName(self.satelliteName.replace('_',' '))
        template.setCenter(BodyFacade("EARTH", CelestialBodyFactory.getCelestialBodies().getEarth()))
        template.setReferenceFrame(FrameFacade.map(J2K_FRAME)) # ALERT: M2K frame hardcoded! Set the reference frame in which data are given: used for state vector and Keplerian elements data (and for the covariance reference frame if none is given)
        # writer
        writer = EphemerisWriter(WriterBuilder().buildOemWriter(), oem_header, template, FileFormat.KVN, "dummy", 60)
        writer.write(output, self.ephemerisFile)

Should I define a custom method, e.g. myKvnGenerator?
How should I call it from my code? maybe in line

EphemerisWriter(WriterBuilder().buildOemWriter(), oem_header, template, FileFormat.KVN, "dummy", 60)

but how?

Thank you very much.
Kind Regards,
Alfredo

Hi Alfredo,

Since you’re using Python, you can always read the file once written by Orekit to rewrite it and change the format of the dates using the datetime builtin library for example.

Best,
Romain.

Dear Serrof,
Thanks for the suggestion, I’ll do that post-processing if I cannot find a way to implement Luc’s solution.
Regards,
Alfredo

Thanks for this tip!

We may use it to write seconds with a stable number of digits: some of our users are wondering why number of digits differs from one line to another.

For information, we quote that even with the same fraction of seconds, the number of digits varies upon the number of seconds in the minute. It might be due to the Floating-point arithmetic.

I write a test to demonstrate this :

  @Test
    public void testIssuePrecision() throws IOException {
        AbsoluteDate start = new AbsoluteDate("1996-12-17T00:00:00.000", TimeScalesFactory.getUTC());
        AbsoluteDate end = start.shiftedBy(120.0);
        Orbit initialOrbit = new KeplerianOrbit(6378137 + 500e3, 1e-3, 0, 0, 0, 0, PositionAngleType.TRUE, FramesFactory.getGCRF(), start, Constants.IERS2010_EARTH_MU);
        OemData data = new OemData();

        Propagator propagator = new KeplerianPropagator(initialOrbit);
        propagator.propagate(end);
        BoundedPropagator ephemeris = propagator.getEphemerisGenerator().getGeneratedEphemeris();
        IntStream.range(0, 10)
                 .mapToObj(i -> start.shiftedBy(i * 10 + 14e-15))
                 .map(date -> ephemeris.propagate(date))
                 .forEach(orbit -> data.addData(orbit.getPVCoordinates(), false));

        CharArrayWriter caw = new CharArrayWriter();
        OemWriter oemWriter = new OemWriter(IERSConventions.IERS_2010, DataContext.getDefault(), start);
        Generator generator = new KvnGenerator(caw, 25, "dummy.kvn", Constants.JULIAN_DAY, 60);
        OdmHeader header = new OdmHeader();
        header.setCreationDate(start);
        header.setFormatVersion(2.0);
        header.setOriginator("TOTO");
        oemWriter.writeHeader(generator, header);
        OemSegment oemSegment = new OemSegment(dummyMetadata(), data, Constants.IERS2010_EARTH_MU);
        oemWriter.writeSegmentContent(generator, 0., oemSegment);
        System.out.println(caw);
        Assertions.assertTrue(
                Arrays.stream(caw.toString().split("\n")).map(line -> line.split(" ")[0]).filter(OemWriterTest::isDouble).map(String::length).distinct().count() == 1);
    }

    private static boolean isDouble(String split) {
        try {
            Double.parseDouble(split);
            return true;
        }
        catch (NumberFormatException e) {
            return false;
        }
    }

    private OemMetadata dummyMetadata() {
        OemMetadata metadata = new OemMetadata(4);
        metadata.addComment("dummy comment");
        metadata.setTimeSystem(TimeSystem.TT);
        metadata.setObjectID("9999-999ZZZ");
        metadata.setObjectName("transgalactic");
        metadata.setCenter(new BodyFacade("EARTH", CelestialBodyFactory.getCelestialBodies().getEarth()));
        metadata.setReferenceFrame(FrameFacade.map(FramesFactory.getEME2000()));
        metadata.setStartTime(AbsoluteDate.J2000_EPOCH.shiftedBy(80 * Constants.JULIAN_CENTURY));
        metadata.setStopTime(metadata.getStartTime().shiftedBy(Constants.JULIAN_YEAR));
        return metadata;
    }

This code gives the following output:

1.4E-14 6871.258863 1.0668314428114478E-13 0.0 -1.1819339052146793E-16 7.620224591510341 0.0
10.000000000000014 6870.836748083066 76.20068549091282 0.0 -0.08442211640495255 7.619756466187601 0.0
20.000000000000014 6869.570455350527 152.39200866881595 0.0 -0.16883382926577173 7.618352148252695 0.0
30.000000000000014 6867.4601408506505 228.5646083813412 0.0 -0.2532247363433602 7.616011811798099 0.0
40.000000000000014 6864.506064642135 304.7091257972543 0.0 -0.3375844380085175 7.612735746953117 0.0
50.000000000000014 6860.708590761485 380.8162055666487 0.0 -0.4219025385464506 7.608524359846628 0.0
60.000000000000014 6856.068187177343 456.8764969806938 0.0 -0.5061686474607596 7.603378172554958 0.0
70.00000000000001 6850.5854257318 532.8806551307864 0.0 -0.5903723807767235 7.597297823034864 0.0
80.00000000000001 6844.260982068653 608.8193420669578 0.0 -0.6745033623437111 7.590284065041638 0.0
90.00000000000001 6837.095635548664 684.6832279553889 0.0 -0.7585512251365432 7.582337768032378 0.0

On an arithmetic point of view, it makes sense, but in practice, it is hard to justify.

Best regards,

Anne-Laure

1 Like