Creating TLEs with parsed parameters in Python wrapper

I am working on creating a small Python script that pulls TLE definitions from Celestrak and/or the Space-Track API and looks for close conjunctions in the sky as seen by an earth station. To future-proof the script, I would like to avoid using the traditional TLE/3LE text format so that there are no issues when 18 SDS starts issuing six digit identifiers. Both Celestrak and Space-Track recommend this approach for new development.

I am pulling down the data in JSON format. Here is a snippet from Celestrak (Space-Track/18 SDS has a very similar format, except it gives the elements as string literals rather than JSON integer/number objects):

    {
        "OBJECT_NAME": "ISS (ZARYA)",
        "OBJECT_ID": "1998-067A",
        "EPOCH": "2024-07-09T11:26:59.754336",
        "MEAN_MOTION": 15.49695979,
        "ECCENTRICITY": 0.0010507,
        "INCLINATION": 51.6384,
        "RA_OF_ASC_NODE": 204.9033,
        "ARG_OF_PERICENTER": 46.5485,
        "MEAN_ANOMALY": 119.156,
        "EPHEMERIS_TYPE": 0,
        "CLASSIFICATION_TYPE": "U",
        "NORAD_CAT_ID": 25544,
        "ELEMENT_SET_NO": 999,
        "REV_AT_EPOCH": 46200,
        "BSTAR": 0.00021577,
        "MEAN_MOTION_DOT": 0.00011765,
        "MEAN_MOTION_DDOT": 0
    }

To convert this JSON object to a TLE object in Orekit, I wrote the following function (note the float() and int() casts are there to handle the string literals from the Space-Track API):

def create_orekit_tle(tle_elems):
    launch_year = int(tle_elems['OBJECT_ID'][0:4])
    launch_num = int(tle_elems['OBJECT_ID'][5:8])
    launch_piece = tle_elems['OBJECT_ID'][8:]
    ephem_type = 0 # always zero for public TLEs
    epoch = datetime_to_absolutedate(datetime.datetime.fromisoformat(tle_elems['EPOCH']))
    tle = TLE(int(tle_elems['NORAD_CAT_ID']), java.lang.Character(tle_elems['CLASSIFICATION_TYPE']), launch_year, launch_num, \
              java.lang.String(launch_piece), ephem_type, int(tle_elems['ELEMENT_SET_NO']), epoch, \
              np.radians(float(tle_elems['MEAN_MOTION'])), np.radians(float(tle_elems['MEAN_MOTION_DOT'])), \
              np.radians(float(tle_elems['MEAN_MOTION_DDOT'])), float(tle_elems['ECCENTRICITY']), \
              np.radians(float(tle_elems['INCLINATION'])), np.radians(float(tle_elems['ARG_OF_PERICENTER'])), \
              np.radians(float(tle_elems['RA_OF_ASC_NODE'])), np.radians(float(tle_elems['MEAN_ANOMALY'])), \
              int(tle_elems['REV_AT_EPOCH']), float(tle_elems['BSTAR']) )
    return tle

This isn’t working - this is the error I get:

InvalidArgsError                          Traceback (most recent call last)
Cell In[4], line 1
----> 1 tle = exclusiontool.create_orekit_tle(tle_dict[25544])

File ~\Documents\Python Scripts\exclusiontool\exclusiontool.py:85, in create_orekit_tle(tle_elems)
     83 ephem_type = 0 # always zero for public TLEs
     84 epoch = datetime_to_absolutedate(datetime.datetime.fromisoformat(tle_elems['EPOCH']))
---> 85 tle = TLE(int(tle_elems['NORAD_CAT_ID']), java.lang.Character(tle_elems['CLASSIFICATION_TYPE']), launch_year, launch_num, \
     86           java.lang.String(launch_piece), ephem_type, int(tle_elems['ELEMENT_SET_NO']), epoch, \
     87           np.radians(float(tle_elems['MEAN_MOTION'])), np.radians(float(tle_elems['MEAN_MOTION_DOT'])), \
     88           np.radians(float(tle_elems['MEAN_MOTION_DDOT'])), float(tle_elems['ECCENTRICITY']), \
     89           np.radians(float(tle_elems['INCLINATION'])), np.radians(float(tle_elems['ARG_OF_PERICENTER'])), \
     90           np.radians(float(tle_elems['RA_OF_ASC_NODE'])), np.radians(float(tle_elems['MEAN_ANOMALY'])), \
     91           int(tle_elems['REV_AT_EPOCH']), float(tle_elems['BSTAR']) )
     92 return tle

InvalidArgsError: (<class 'org.orekit.propagation.analytical.tle.TLE'>, '__init__', (25544, <Character: U>, 1998, 67, <String: A>, 0, 999, <AbsoluteDate: 2024-07-09T11:26:59.754336Z>, 0.270472972384669, 2.0533798649713286e-06, 0.0, 0.0010507, 0.9012601004618398, 0.8124245868645804, 3.576237233201697, 2.0796645235063633, 46200, 0.00021577))

Any thoughts? Note that I originally had the classification and launchPiece arguments as Python string types. I tried converting them to java.lang.Character and java.lang.String objects respectively, but no luck.

Hi,

The wrapper doesn’t like numpy types. Please cast all numbers in native python classes e.g. float, int, etc. So typically on the result of radians

Cheers,
Romain.

Hi Romain,

Thanks! That did it - I can use the .item() method of the numpy type to return the native Python type. So, this works for creating a TLE object:

def create_orekit_tle(tle_elems):
    launch_year = int(tle_elems['OBJECT_ID'][0:4])
    launch_num = int(tle_elems['OBJECT_ID'][5:8])
    launch_piece = tle_elems['OBJECT_ID'][8:]
    ephem_type = 0 # always zero for public TLEs
    epoch = datetime_to_absolutedate(datetime.datetime.fromisoformat(tle_elems['EPOCH']))
    tle = TLE(int(tle_elems['NORAD_CAT_ID']),
              tle_elems['CLASSIFICATION_TYPE'],
              launch_year,
              launch_num,
              launch_piece,
              ephem_type,
              int(tle_elems['ELEMENT_SET_NO']),
              epoch,
              np.radians(float(tle_elems['MEAN_MOTION'])).item(),
              np.radians(float(tle_elems['MEAN_MOTION_DOT'])).item(),
              np.radians(float(tle_elems['MEAN_MOTION_DDOT'])).item(),
              float(tle_elems['ECCENTRICITY']),
              np.radians(float(tle_elems['INCLINATION'])).item(),
              np.radians(float(tle_elems['ARG_OF_PERICENTER'])).item(),
              np.radians(float(tle_elems['RA_OF_ASC_NODE'])).item(),
              np.radians(float(tle_elems['MEAN_ANOMALY'])).item(),
              int(tle_elems['REV_AT_EPOCH']),
              float(tle_elems['BSTAR']) )
    return tle

However, now I am having an issue with the mean motion and its derivatives (MEAN_MOTION, MEAN_MOTION_DOT, MEAN_MOTION_DDOT). I skipped over the part in the documentation where the constructor is expecting them in rad/s (or rad/s^2 and rad/s^3). The data is distributed in degrees/day (and deg/day^2 and deg/day^3). So, that naturally returns an error showing that the mean motion is out of range:

In [4]: tle
Out[4]: Exception in thread "main" org.orekit.errors.OrekitException: invalid TLE parameter for object 25,544: meanMotion = 3719.27034960
        at org.orekit.propagation.analytical.tle.ParseUtils.addPadding(ParseUtils.java:122)
        at org.orekit.propagation.analytical.tle.TLE.buildLine2(TLE.java:560)
        at org.orekit.propagation.analytical.tle.TLE.getLine2(TLE.java:441)
        at org.orekit.propagation.analytical.tle.TLE.toString(TLE.java:710)

To remedy this, I’m trying to convert by dividing by the number of seconds per day (and the same values squared and cubed for DOT and DDOT respectively):

def create_orekit_tle(tle_elems):
   launch_year = int(tle_elems['OBJECT_ID'][0:4])
   launch_num = int(tle_elems['OBJECT_ID'][5:8])
   launch_piece = tle_elems['OBJECT_ID'][8:]
   ephem_type = 0 # always zero for public TLEs
   epoch = datetime_to_absolutedate(datetime.datetime.fromisoformat(tle_elems['EPOCH']))
   tle = TLE(int(tle_elems['NORAD_CAT_ID']),
             tle_elems['CLASSIFICATION_TYPE'],
             launch_year,
             launch_num,
             launch_piece,
             ephem_type,
             int(tle_elems['ELEMENT_SET_NO']),
             epoch,
             (np.radians(float(tle_elems['MEAN_MOTION'])) / 86400.0).item(),
             (np.radians(float(tle_elems['MEAN_MOTION_DOT'])) / 86400.0**2).item(),
             (np.radians(float(tle_elems['MEAN_MOTION_DDOT'])) / 86400.0**3).item(),
             float(tle_elems['ECCENTRICITY']),
             np.radians(float(tle_elems['INCLINATION'])).item(),
             np.radians(float(tle_elems['ARG_OF_PERICENTER'])).item(),
             np.radians(float(tle_elems['RA_OF_ASC_NODE'])).item(),
             np.radians(float(tle_elems['MEAN_ANOMALY'])).item(),
             int(tle_elems['REV_AT_EPOCH']),
             float(tle_elems['BSTAR']) )
   return tle

However, when I print the output, the mean motion, mean motion derivative, and mean motion second derivative fields are way off:

In [4]: tle
Out[4]:
<TLE: 1 25544U 98067A   24191.47708049  .00000016  00000-0  21577-3 0  9998
2 25544  51.6384 204.9033 0010507  46.5485 119.1560  0.04304711462000>

For reference, the current TLE for this object from Celestrak is:

ISS (ZARYA)             
1 25544U 98067A   24191.47708049  .00011765  00000+0  21577-3 0  9990
2 25544  51.6384 204.9033 0010507  46.5485 119.1560 15.49695979462004

So, the mean motion should be 15.49695979462004 (showing 0.04304711462000) and the first derivative should be .00011765 (showing .00000016). The second derivative is zero in both cases, however, this is likely only because DDOT for this object is also zero.

I assume I must just be making a silly mistake somewhere.

I fixed it. I knew it would be something obvious. Mean motion is in revolutions per day, not degrees per day (that should have been blatantly obvious looking at the numbers - not enough coffee today). Also, MEAN_MOTION_DOT is one half the derivative of mean motion, and MEAN_MOTION_DDOT is one sixth the second derivative of mean motion.

So, if anyone else is having this issue, this Python function should work on a JSON object from Celestrak parsed to a Python dict:

def create_orekit_tle(tle_elems):
    launch_year = int(tle_elems['OBJECT_ID'][0:4])
    launch_num = int(tle_elems['OBJECT_ID'][5:8])
    launch_piece = tle_elems['OBJECT_ID'][8:]
    ephem_type = 0 # always zero for public TLEs
    epoch = datetime_to_absolutedate(datetime.datetime.fromisoformat(tle_elems['EPOCH']))
    tle = TLE(int(tle_elems['NORAD_CAT_ID']),
              tle_elems['CLASSIFICATION_TYPE'],
              launch_year,
              launch_num,
              launch_piece,
              ephem_type,
              int(tle_elems['ELEMENT_SET_NO']),
              epoch,
              2.0*np.pi*(float(tle_elems['MEAN_MOTION'])) / 86400.0,
              4.0*np.pi*(float(tle_elems['MEAN_MOTION_DOT'])) / 86400.0**2,
              12.0*np.pi*(float(tle_elems['MEAN_MOTION_DDOT'])) / 86400.0**3,
              float(tle_elems['ECCENTRICITY']),
              np.radians(float(tle_elems['INCLINATION'])).item(),
              np.radians(float(tle_elems['ARG_OF_PERICENTER'])).item(),
              np.radians(float(tle_elems['RA_OF_ASC_NODE'])).item(),
              np.radians(float(tle_elems['MEAN_ANOMALY'])).item(),
              int(tle_elems['REV_AT_EPOCH']),
              float(tle_elems['BSTAR']) )
    return tle

Here is the output for the ISS:

1 25544U 98067A   24191.47708049  .00011765  00000-0  21577-3 0  9991
2 25544  51.6384 204.9033 0010507  46.5485 119.1560 15.49695979462004

And here is the reference version from Celestrak:

ISS (ZARYA)             
1 25544U 98067A   24191.47708049  .00011765  00000+0  21577-3 0  9990
2 25544  51.6384 204.9033 0010507  46.5485 119.1560 15.49695979462004

They are identical, save for the sign on the second derivative mean motion being negative on the first line (which also changes the checksum digit). This is likely just a display issue and is essentially irrelevant.

2 Likes