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: