Issues while using stateToTLE method

Hello,

I’m trying to generate TLEs from spacecraft state, hence trying to use the stateToTLE method. But I end up creating TLEs that actually don’t meet my expectations while working on a simple case (as a way to validate my future work).
For instance, while creating a simple Keplerian orbit, generating states, and using the stateToTLE method, the Keplerian elements contained in the resulting TLEs are varying quite a lot, though it would be expected for it to be constant.

The code I’m using for that simple case :

a_in_meters_without_earth_radius = 600000.0
e = 0.001
i_in_degrees = 51.0
aop_in_degrees = 20.0
raan_in_degrees = 10.0
ta_in_degrees = 0.0

ITRF = FramesFactory.getITRF(IERSConventions.IERS_2010, True)
utc = TimeScalesFactory.getUTC()

inertialFrame = FramesFactory.getEME2000()

earth = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
                        Constants.WGS84_EARTH_FLATTENING,
                        FramesFactory.getITRF(IERSConventions.IERS_2010, True))

a = Constants.WGS84_EARTH_EQUATORIAL_RADIUS + a_in_meters_without_earth_radius  # semi-major axis in meters
i = math.radians(i_in_degrees)                                                  # inclination
aop = math.radians(aop_in_degrees)                                              # perigee argument
raan = math.radians(raan_in_degrees)                                            # right ascension of ascending node
ta = math.radians(ta_in_degrees)                                                # true anomaly

initialDate = AbsoluteDate(2025, 5, 9, 12, 0, 0.0, utc)

initialOrbit = KeplerianOrbit(a, e, i, aop, raan, ta, PositionAngleType.TRUE,
                             inertialFrame, initialDate, Constants.WGS84_EARTH_MU)

# Attitude provider: aligned with orbital frame
lof = LOFType.VNC
attitudeLaw = LofOffset(inertialFrame, lof)

# Create propagator
propagator = KeplerianPropagator(initialOrbit, attitudeLaw)

finalDate = initialDate.shiftedBy(4 * 3600.0)

# Propagate and collect states
step = 60.0  # 60 seconds step
spacecraft_states = []

currentDate = initialDate
while currentDate.compareTo(finalDate) <= 0:
    state = propagator.propagate(currentDate)
    spacecraft_states.append(state)
    currentDate = currentDate.shiftedBy(step)

my usage of the stateToTLE method :

def a_without_earth_radius_to_mean_motion(a):
    n_rad_s = math.sqrt(Constants.WGS84_EARTH_MU / (a+Constants.WGS84_EARTH_EQUATORIAL_RADIUS)**3)
    return n_rad_s * 86400 / (2 * math.pi)

L_TLE = list()
fixedPoint = FixedPointTleGenerationAlgorithm()

norad_id = 99985 # Dummy norad
TLE_template = TLE(
    f"1 {norad_id}U {norad_id}A   25129.50000000  .00000000  00000-0  00000-0 0    10",
    f"2 {norad_id}  {format(i_in_degrees, '0>7.4f')} {format(raan_in_degrees, '0>8.4f')} {format(int(e*1e7),'0>7')} {format(aop_in_degrees, '0>8.4f')} 000.0000 {format(a_without_earth_radius_to_mean_motion(a_in_meters_without_earth_radius), '0>11.8f')}000000"
)

for state in spacecraft_states:
    L_TLE.append(TLE.stateToTLE(state, TLE_template, fixedPoint))

And I extract the Keplerian elements from the spacecraft states and from the TLEs in the following ways, to compare them (see the attached excel file) :

# Spacecraft states data
L_a = list()
L_e = list()
L_i_rad = list()
L_pa_rad = list()
L_raan_rad = list()
L_ma_rad = list()

for state in spacecraft_states:
    orbit = KeplerianOrbit(state.getOrbit())
    L_a.append(orbit.getA())
    L_e.append(orbit.getE())
    L_i_rad.append(orbit.getI())
    L_pa_rad.append(orbit.getPerigeeArgument())
    L_raan_rad.append(orbit.getRightAscensionOfAscendingNode())
    L_ma_rad.append(orbit.getMeanAnomaly())

L_i = [e * 180/math.pi for e in L_i_rad]
L_pa = [e * 180/math.pi for e in L_pa_rad]
L_raan = [e * 180/math.pi for e in L_raan_rad]
L_ma = [e * 180/math.pi for e in L_ma_rad]

# TLE data
L_a_TLE = [mean_motion_to_a(TLE.getMeanMotion()) for TLE in L_TLE]
L_e_TLE = [TLE.getE() for TLE in L_TLE]
L_i_rad_TLE = [TLE.getI() for TLE in L_TLE]
L_pa_rad_TLE = [TLE.getPerigeeArgument() for TLE in L_TLE]
L_raan_rad_TLE = [TLE.getRaan() for TLE in L_TLE]
L_ma_rad_TLE = [TLE.getMeanAnomaly() for TLE in L_TLE]

L_i_TLE = [e * 180/math.pi for e in L_i_rad_TLE]
L_pa_TLE = [e * 180/math.pi for e in L_pa_rad_TLE]
L_raan_TLE = [e * 180/math.pi for e in L_raan_rad_TLE]
L_ma_TLE = [e * 180/math.pi for e in L_ma_rad_TLE]

compare_elements.xlsx (68.9 KB)

That “noise” I end up with is very bothersome for what I’m trying to achieve as a final result, as it creates a lot of uncertainty - is that a known issue/behaviour and/or am I missing something? Is it possible to fix this, or maybe I’m not working the right way?

Thank you very much for the possible explanations you would give me! :slightly_smiling_face:

Hello again!
If anyone could help me on this I’d be very grateful! :slightly_smiling_face:

Hi @charles.lstg,

I haven’t had time to read all your code but maybe the problem comes from the fact that you’re comparing osculating elements (from the Keplerian propagator) with mean elements in the sense of SGP4 theory (from the TLEs).
So it’s logical that you don’t get the same values.

Does that help you?

Cheers,
Maxime