Add adaptive step limiting to DC solver

Scale step limits by a trust-region factor that halves on divergence
(RMS increases > 1%) and relaxes toward full step on good convergence
(RMS decreases > 10%). Prevents oscillation with poor initial guesses
without affecting well-seeded fits. Also stores SVD condition number
for diagnostic use in upcoming covariance output.

Existing 8 OD regression tests + 67 standalone math tests unaffected
(adaptive_factor starts at 1.0, round-trip tests never trigger
divergence).
This commit is contained in:
Ryan Malloy 2026-02-17 15:55:20 -07:00
parent 87ab81e7d0
commit 9b0634725b
2 changed files with 32 additions and 8 deletions

View File

@ -332,7 +332,8 @@ compute_residuals_topo(const tle_t *tle, const od_observation_t *obs,
* operates on a different scale.
*/
static void
apply_step_limits(double *dx, int nstate, const od_equinoctial_t *eq)
apply_step_limits(double *dx, int nstate, const od_equinoctial_t *eq,
double adaptive_factor)
{
/* Step limits for equinoctial elements (Vallado 2008 Table 2) */
static const double limits_6[6] = {
@ -347,7 +348,8 @@ apply_step_limits(double *dx, int nstate, const od_equinoctial_t *eq)
int i;
double ratio, max_ratio = 0.0;
/* Find the maximum ratio of correction to limit */
/* Find the maximum ratio of correction to limit, scaled by
* adaptive_factor (trust-region-like damping when diverging) */
for (i = 0; i < 6; i++)
{
double scale;
@ -356,7 +358,7 @@ apply_step_limits(double *dx, int nstate, const od_equinoctial_t *eq)
else
scale = 1.0;
ratio = fabs(dx[i]) / (limits_6[i] * scale);
ratio = fabs(dx[i]) / (limits_6[i] * scale * adaptive_factor);
if (ratio > max_ratio)
max_ratio = ratio;
}
@ -373,8 +375,8 @@ apply_step_limits(double *dx, int nstate, const od_equinoctial_t *eq)
if (nstate == OD_NSTATE_7)
{
double bstar_limit = fabs(eq->bstar) > 1e-10
? fabs(eq->bstar) * 0.4
: 1e-5;
? fabs(eq->bstar) * 0.4 * adaptive_factor
: 1e-5 * adaptive_factor;
if (fabs(dx[6]) > bstar_limit)
dx[6] = (dx[6] > 0.0 ? bstar_limit : -bstar_limit);
}
@ -477,6 +479,8 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
double *sv = NULL;
double *work = NULL;
double rms_prev, rms_cur;
double adaptive_factor = 1.0; /* trust-region damping */
double condition_number = 0.0; /* s_max / s_min from SVD */
int iter;
int max_iter;
int converged = 0;
@ -682,8 +686,12 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
break;
}
/* Apply step limiting */
apply_step_limits(dx, nstate, &eq);
/* Store condition number from SVD singular values */
if (sv[nstate - 1] > 1e-30)
condition_number = sv[0] / sv[nstate - 1];
/* Apply step limiting (scaled by adaptive factor) */
apply_step_limits(dx, nstate, &eq, adaptive_factor);
/* Update equinoctial elements */
eq.n += dx[0];
@ -716,6 +724,21 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
break;
}
/* Adaptive step control: adjust trust-region size based on
* whether the solver is converging or diverging. Halve the
* step on divergence (prevents oscillation with poor initial
* guesses), relax toward full step when converging well. */
if (rms_cur > rms_prev * 1.01)
{
adaptive_factor *= 0.5;
if (adaptive_factor < 0.05)
adaptive_factor = 0.05;
}
else if (rms_cur < rms_prev * 0.9)
{
adaptive_factor = fmin(adaptive_factor * 1.3, 1.0);
}
/* Check convergence: either absolute RMS is small enough,
* or relative change has plateaued (solver found its best fit,
* common for SDP4 deep-space orbits where the numerical floor
@ -749,6 +772,8 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
else if (iter >= max_iter && result->status[0] == '\0')
snprintf(result->status, sizeof(result->status), "max_iterations");
(void)condition_number; /* used by covariance output (v0.5.0) */
/* Cleanup */
od_free(residuals);
od_free(jacobian);

View File

@ -3,7 +3,6 @@
-- Tests tle_from_eci(), tle_from_topocentric(), and tle_fit_residuals().
-- Uses round-trip methodology: propagate known TLE → fit from obs → compare.
CREATE EXTENSION IF NOT EXISTS pg_orrery;
NOTICE: extension "pg_orrery" already exists, skipping
-- ============================================================
-- Test 1: ECI round-trip (ISS-like LEO orbit)
--