@@ -14,6 +14,8 @@ public class MeanElementsConverter
1414 const double SAFETY_ALTITUDE = 120000.0 ; // 120 km minimum altitude for LEO
1515 const double EQUATORIAL_RADIUS_EARTH = 6378136.6 ; // meters
1616 const double POSITION_TOLERANCE = 1E+04 ; // 10 km position tolerance for best-effort fallback
17+ const int MAX_NON_IMPROVING_ITERATIONS = 50 ; // Increased from 20
18+ const int MIN_ITERATIONS_BEFORE_EARLY_EXIT = 50 ; // Don't exit early before 50 iterations
1719
1820 /// <summary>
1921 /// Fit a TLE from a state vector using the upgraded
@@ -37,9 +39,19 @@ internal OrbitalParameters Convert(
3739 }
3840
3941 /// <summary>
40- /// Convert using Equinoctial elements for better numerical stability with circular/equatorial orbits
41- /// Following FixedPointConverter approach
42+ /// Convert using Equinoctial elements for better numerical stability and convergence
4243 /// </summary>
44+ /// <param name="osculatingElements"></param>
45+ /// <param name="noradId"></param>
46+ /// <param name="name"></param>
47+ /// <param name="cosparId"></param>
48+ /// <param name="revAtEpoch"></param>
49+ /// <param name="bstar"></param>
50+ /// <param name="epsilon"></param>
51+ /// <param name="maxIterations"></param>
52+ /// <param name="scale"></param>
53+ /// <returns></returns>
54+ /// <exception cref="InvalidOperationException"></exception>
4355 private OrbitalParameters ConvertUsingEquinoctialElements (
4456 StateVector osculatingElements ,
4557 ushort noradId ,
@@ -51,25 +63,37 @@ private OrbitalParameters ConvertUsingEquinoctialElements(
5163 int maxIterations ,
5264 double scale )
5365 {
54- // Convert target to Equinoctial elements for numerical stability
55- var targetEquinoctial = osculatingElements . ToEquinoctial ( ) ;
56-
57- // Initial guess: use target equinoctial elements directly
58- var currentEquinoctial = new EquinoctialElements (
59- targetEquinoctial . P , targetEquinoctial . F , targetEquinoctial . G ,
60- targetEquinoctial . H , targetEquinoctial . K , targetEquinoctial . L0 ,
61- osculatingElements . Observer , osculatingElements . Epoch , osculatingElements . Frame ) ;
66+ // Convert target to Equinoctial elements for numerical stability (reuse directly)
67+ var currentEquinoctial = osculatingElements . ToEquinoctial ( ) ;
68+ var targetEquinoctial = currentEquinoctial ;
6269
6370 double bestPositionError = double . MaxValue ;
6471 OrbitalParameters bestTle = null ;
6572
66- // Compute thresholds based on orbit size and speed (meters and m/s)
73+ // Cache frequently accessed values outside the loop
6774 var mu = osculatingElements . Observer . GM ;
75+ var observer = osculatingElements . Observer ;
76+ var epoch = osculatingElements . Epoch ;
77+ var frame = osculatingElements . Frame ;
78+ var targetPosition = osculatingElements . Position ;
79+ var targetVelocity = osculatingElements . Velocity ;
80+ var celestialBody = observer as CelestialBody ;
81+ var minBodyRadius = celestialBody ? . EquatorialRadius ?? EQUATORIAL_RADIUS_EARTH ;
82+ var rpMin = minBodyRadius + SAFETY_ALTITUDE ;
83+
84+ // Compute thresholds based on orbit size and speed (meters and m/s)
6885 var aGuess = targetEquinoctial . SemiMajorAxis ( ) ;
6986 var vScale = System . Math . Sqrt ( mu / aGuess ) ;
7087 var positionThreshold = epsilon * aGuess ; // e.g., 1e-10 * a ~ sub-millimeter for LEO
7188 var velocityThreshold = epsilon * vScale ; // consistent velocity scale
7289
90+ // Reusable arrays to avoid allocations
91+ Span < double > residuals = stackalloc double [ 6 ] ;
92+
93+ // Track non-improving iterations for early exit (but only after some minimum iterations)
94+ int nonImprovingIterations = 0 ;
95+
96+
7397 for ( int iter = 0 ; iter < maxIterations ; iter ++ )
7498 {
7599 try
@@ -78,19 +102,29 @@ private OrbitalParameters ConvertUsingEquinoctialElements(
78102 var testTle = TLE . Create ( currentEquinoctial , name , noradId , cosparId , revAtEpoch , Classification . Unclassified , bstar ) ;
79103
80104 // Propagate TLE and convert back to equinoctial for comparison
81- var propagatedState = testTle . ToStateVector ( osculatingElements . Epoch ) ;
82- var propagatedEquinoctial = propagatedState . ToEquinoctial ( ) ;
83-
84- // Compute position/velocity errors for convergence check
85- var posVec = osculatingElements . Position - propagatedState . Position ;
86- var velVec = osculatingElements . Velocity - propagatedState . Velocity ;
105+ var propagatedState = testTle . ToStateVector ( epoch ) ;
106+
107+ // Compute position/velocity errors for convergence check (before expensive ToEquinoctial)
108+ var posVec = targetPosition - propagatedState . Position ;
109+ var velVec = targetVelocity - propagatedState . Velocity ;
87110 var positionError = posVec . Magnitude ( ) ;
88111 var velocityError = velVec . Magnitude ( ) ;
89112
113+ // Track best solution
90114 if ( positionError < bestPositionError )
91115 {
92116 bestPositionError = positionError ;
93117 bestTle = testTle ;
118+ nonImprovingIterations = 0 ;
119+ }
120+ else
121+ {
122+ nonImprovingIterations ++ ;
123+ // Early exit only after minimum iterations and if really not improving
124+ if ( iter >= MIN_ITERATIONS_BEFORE_EARLY_EXIT && nonImprovingIterations >= MAX_NON_IMPROVING_ITERATIONS )
125+ {
126+ break ;
127+ }
94128 }
95129
96130 // Check convergence using position AND velocity criteria
@@ -99,41 +133,33 @@ private OrbitalParameters ConvertUsingEquinoctialElements(
99133 return testTle ;
100134 }
101135
102- // Compute residuals in equinoctial space (no singularities!)
103- var residuals = ComputeEquinoctialResiduals ( targetEquinoctial , propagatedEquinoctial ) ;
136+ // Only compute equinoctial if we need to continue iterating
137+ var propagatedEquinoctial = propagatedState . ToEquinoctial ( ) ;
104138
105- // Basic component-wise scaling to account for units and coupling
106- // Scale P residual by semi-major axis to keep steps moderate
107- var pScale = System . Math . Max ( 1.0 , aGuess ) ;
108- var fScale = 1.0 ; // dimensionless
109- var gScale = 1.0 ; // dimensionless
110- var hScale = 1.0 ; // dimensionless
111- var kScale = 1.0 ; // dimensionless
112- var lScale = 1.0 ; // radians
139+ // Compute residuals in equinoctial space (no singularities!)
140+ ComputeEquinoctialResidualsInPlace ( targetEquinoctial , propagatedEquinoctial , residuals ) ;
113141
114142 // Fixed-point correction with adaptive scaling
115143 var adaptiveScale = scale * System . Math . Max ( 0.1 , 1.0 - ( double ) iter / maxIterations ) ;
116144
117145 // Apply corrections to equinoctial elements
118- var newP = currentEquinoctial . P - adaptiveScale * ( residuals [ 0 ] / pScale ) * pScale ;
119- var newF = currentEquinoctial . F - adaptiveScale * ( residuals [ 1 ] / fScale ) * fScale ;
120- var newG = currentEquinoctial . G - adaptiveScale * ( residuals [ 2 ] / gScale ) * gScale ;
121- var newH = currentEquinoctial . H - adaptiveScale * ( residuals [ 3 ] / hScale ) * hScale ;
122- var newK = currentEquinoctial . K - adaptiveScale * ( residuals [ 4 ] / kScale ) * kScale ;
123- var newL0 = SpecialFunctions . NormalizeAngle ( currentEquinoctial . L0 - adaptiveScale * ( residuals [ 5 ] / lScale ) * lScale ) ;
146+ var newP = currentEquinoctial . P - adaptiveScale * residuals [ 0 ] ;
147+ var newF = currentEquinoctial . F - adaptiveScale * residuals [ 1 ] ;
148+ var newG = currentEquinoctial . G - adaptiveScale * residuals [ 2 ] ;
149+ var newH = currentEquinoctial . H - adaptiveScale * residuals [ 3 ] ;
150+ var newK = currentEquinoctial . K - adaptiveScale * residuals [ 4 ] ;
151+ var newL0 = SpecialFunctions . NormalizeAngle ( currentEquinoctial . L0 - adaptiveScale * residuals [ 5 ] ) ;
124152
125153 // Enforce physical constraints via perigee constraint using P,F,G
126154 // a = P/(1-e^2), rp = a(1-e) must be >= minRadius + safetyAltitude
127155 var e2 = newF * newF + newG * newG ;
128156 var e = System . Math . Sqrt ( System . Math . Max ( 0.0 , System . Math . Min ( e2 , 0.999999 ) ) ) ; // keep e < 1
129- var celestialBody = osculatingElements . Observer as CelestialBody ;
130- var minBodyRadius = celestialBody ? . EquatorialRadius ?? EQUATORIAL_RADIUS_EARTH ;
131157
132158 // Recompute a from P and e, then check perigee
133159 var denom = System . Math . Max ( 1e-12 , 1.0 - e * e ) ;
134160 var aNow = newP / denom ;
135161 var rp = aNow * ( 1.0 - e ) ;
136- var rpMin = minBodyRadius + SAFETY_ALTITUDE ;
162+
137163 if ( rp < rpMin )
138164 {
139165 // Increase a to satisfy perigee constraint while keeping e
@@ -148,10 +174,12 @@ private OrbitalParameters ConvertUsingEquinoctialElements(
148174 // Create new equinoctial elements for next iteration
149175 currentEquinoctial = new EquinoctialElements (
150176 newP , newF , newG , newH , newK , newL0 ,
151- osculatingElements . Observer , osculatingElements . Epoch , osculatingElements . Frame ) ;
177+ observer , epoch , frame ) ;
152178 }
153- catch ( Exception )
179+ catch ( Exception ex )
154180 {
181+ // Log specific error for debugging
182+ System . Diagnostics . Debug . WriteLine ( $ "TLE conversion iteration { iter } failed: { ex . Message } ") ;
155183 break ;
156184 }
157185 }
@@ -167,19 +195,17 @@ private OrbitalParameters ConvertUsingEquinoctialElements(
167195 }
168196
169197 /// <summary>
170- /// Compute residuals between target and propagated Equinoctial elements
198+ /// Compute residuals between target and propagated Equinoctial elements in-place
199+ /// to avoid array allocation overhead
171200 /// </summary>
172- private static double [ ] ComputeEquinoctialResiduals ( EquinoctialElements target , EquinoctialElements propagated )
201+ private static void ComputeEquinoctialResidualsInPlace ( EquinoctialElements target , EquinoctialElements propagated , Span < double > residuals )
173202 {
174- return
175- [
176- propagated . P - target . P ,
177- propagated . F - target . F ,
178- propagated . G - target . G ,
179- propagated . H - target . H ,
180- propagated . K - target . K ,
181- SpecialFunctions . NormalizeAngleDifference ( propagated . L0 - target . L0 )
182- ] ;
203+ residuals [ 0 ] = propagated . P - target . P ;
204+ residuals [ 1 ] = propagated . F - target . F ;
205+ residuals [ 2 ] = propagated . G - target . G ;
206+ residuals [ 3 ] = propagated . H - target . H ;
207+ residuals [ 4 ] = propagated . K - target . K ;
208+ residuals [ 5 ] = SpecialFunctions . NormalizeAngleDifference ( propagated . L0 - target . L0 ) ;
183209 }
184210 }
185211}
0 commit comments