Skip to content

DynamicMassFit final azim elev - #95

Open
AstroEloy wants to merge 17 commits into
wmpg:masterfrom
AstroEloy:dynmassfit-final-azim-elev
Open

AstroEloy wants to merge 17 commits into
wmpg:masterfrom
AstroEloy:dynmassfit-final-azim-elev

Conversation

@AstroEloy

Copy link
Copy Markdown
Contributor

Summary

  • DynamicMassFit.computeFragEndParams() returns the final lat/lon/height of the propagated
    fragment. It now also returns the apparent ground-fixed radiant azimuth and elevation at that
    point, which dark flight needs as input. Both are printed in the CLI summary for the three mass
    cases.
  • SampleTrajectoryPositions.sampleTrajectory() precessed the derotated radiant from J2000 to the
    epoch of date before converting it to azimuth/elevation. The radiant is already in the epoch of
    date, so precession was applied twice. That step and its imports are removed.

Why the coordinates are already in the epoch of date

  • Station positions come from geo2Cartesian() with its default precess_j2000=False.
  • Lines of sight come from altAz2RADec(): RA = LST - HA, with apparent sidereal time.
  • raDec2AltAz() uses LST directly and expects epoch-of-date RA/Dec.
  • Orbit.calcOrbit() performs the same derotation and conversion without precession, and labels
    the result "apparent ground-fixed, epoch of date".
  • SampleTrajectoryPositions itself prints "The horizontal coordinates are apparent above a fixed
    ground, not topocentric in J2000!".

Why at the final point

The local horizon rotates along the path by ~0.009 deg per km of ground track, so
traj.orbit.azimuth/elevation_apparent_norot, evaluated at the reference point, is not valid at
the end. The difference is tenths of a degree for final points tens of km downrange, and several
degrees for shallow trajectories propagated hundreds of km.

Method

DynamicMassFit uses the same derotation as SampleTrajectoryPositions and Orbit.calcOrbit(),
evaluated at the final point and time. It uses the Earth rotation velocity and east direction at
that point, the radiant from traj.radiant_eci_mini, and the final JD and lat/lon for the
conversion, without precession.

The derotation uses the average speed v_avg_norot, as SampleTrajectoryPositions does. Drag acts
on the air-relative velocity, so the air-relative direction is conserved and the path is straight in
the Earth-fixed frame. Recovering that direction from a straight-line ECI fit requires an average
speed over the fitted segment. This was tested on synthetic fireballs with a known air-relative
direction, fitted as the solver does. Average speeds recover the direction to <= 0.02 deg for a slow,
strongly decelerating fireball, and to <= 0.005 deg for medium and fast ones. v_init or the
terminal ~3 km/s speed would be off by ~1 deg in the slow case.

Validation

The reference was computed independently of wmpl with pyerfa (IAU SOFA), from kinematics only: the
inertial velocity rotated to Earth-fixed with the Greenwich apparent sidereal time, minus w x r,
projected onto the local geodetic ENU basis, with no RA/Dec conversions and no derotation formula.
It agrees with astropy to 0.4" and with a finite difference of Earth-fixed positions to 3e-6 deg.

On several real trajectory solutions (slow and fast fireballs, both hemispheres):

  • Stored station ECI positions match epoch-of-date coordinates to <= 0.13". In J2000 they would be
    0.14-0.34 deg (16-37 km) away, so the trajectory frame is epoch of date.
  • traj.orbit.azimuth/elevation_apparent_norot matches the reference to < 5e-6 deg.
  • DynamicMassFit: the returned final azimuth/elevation matches the reference to <= 1e-4 deg.
  • SampleTrajectoryPositions: before this PR it is off by 0.13-0.54 deg in azimuth or elevation at
    every sample. That output matches reading the radiant as J2000 (to ~0.01 deg, i.e. nutation),
    not as epoch of date. After this PR it matches the reference to <= 1e-4 deg.

The removed error has the signature of precession. For a fixed ground-fixed direction evaluated
between 1980 and 2050, it vanishes at J2000, changes sign across it, and grows at 48.65"/yr and
35.45"/yr for two test directions. General precession predicts 48.64"/yr and 35.51"/yr (50.29"/yr
times the sine of the angle to the ecliptic pole). Without the step, the error stays <= 4e-5 deg over
the whole range.

TrajectoryKML only uses the sampled lat/lon/height, so it is unaffected. Both CLIs run end to end.

🤖 Generated with [Claude Code]

AstroEloy and others added 17 commits September 17, 2026 11:16
…ynamicMassFit

computeFragEndParams() already returns the final lat/lon/height; it now also
returns the apparent ground-fixed radiant azimuth and elevation there, which
dark flight needs as input. The local horizon rotates along the path, so the
values at the reference point are not valid at the end.

The derotation follows SampleTrajectoryPositions and Orbit.calcOrbit, without
a J2000 -> epoch-of-date precession step: the trajectory ECI coordinates are
already in the epoch of date.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion

The trajectory ECI coordinates are in the epoch of date (stations from
geo2Cartesian, lines of sight from altAz2RADec) and raDec2AltAz expects
epoch-of-date coordinates. Precessing the derotated radiant from J2000 to
the date applied precession a second time, biasing the ground-fixed
azimuth/elevation by the general precession accumulated since J2000
(~50"/yr). Without that step the output matches the apparent ground-fixed
radiant reported by Orbit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ssFit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
runFragSim() received gamma_a but ignored it, propagating the fragment with a
hardcoded Gamma*A = 0.7*1.21 = 0.847 while the dynamic mass was computed with
gamma_a (default 0.55). The simulation therefore started with a deceleration
0.847/gamma_a times the measured one. It now uses gamma = 1 and
shape_factor = gamma_a, as dynamicMass() does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…odel

The covariance was built from least_squares' res.jac, which is the Jacobian
reweighted by the soft_l1 loss, not the Jacobian of the linear model, and
then scaled by the robust (MAD) residual variance. This inflated the
deceleration uncertainty (by ~25% in a test case), and with it the +/-2 sigma
dynamic mass interval, since the mass scales as deceleration^-3. It now uses
the linear model Jacobian. Also drops a duplicated MAD = 0 fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fitVelocity() already starts with its own robust fit, so the preceding
_robust_linear_fit() call only provided a starting point for an identical
fit. The result is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The outlier plot label said 5 sigma regardless of --sigma_clip (default 3).
The sigma_clip docstring described a slope-uncertainty threshold, but the
clipping uses the robust (MAD) residual standard deviation. The --maxvel help
said there is no filter by default, but velocities above 73 km/s are always
removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The +/-2 sigma dynamic mass interval only propagates the deceleration
uncertainty. Velocity, atmospheric density, bulk density and Gamma*A are not
included, although the mass scales as v^6 and rho_atm^3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When the evaluation velocity is already below 3 km/s the fragment
simulations are skipped, but the plot label and the final summary still used
the final mass and coordinates, raising a NameError. They now default to NaN.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The radiant fitted by the trajectory solver is the initial tangent of the
path (the solver models gravity as a drop from that line), and drag does not
rotate the direction of motion, but gravity steepens it at a rate
g*cos(elev)/v. The final elevation is now steepened by that turn, integrated
with the average speed over the observed part and the simulated speeds after
it. In a test case this adds 0.27 deg, matching an integration over the
observed velocity profile to 0.005 deg. The azimuth is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This reverts commit 7808ca1. The fixed Gamma*A in runFragSim() is on purpose.
The single-body equations only depend on Gamma*A/m^(1/3), which the dynamic
mass fixes from the measured deceleration, so simulating with the same
Gamma*A as the dynamic mass gives an identical velocity and path for any
gamma_a (only the mass scales as Gamma*A^3). A fixed simulation Gamma*A is
what lets gamma_a be calibrated against the observed final velocity. Adds a
comment stating this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The solver models gravity as a drop from the fitted line, so
traj.radiant_eci_mini is the tangent of the path at its beginning, and
Orbit.calcOrbit() derotates it with v_init. In an end-to-end test (a fireball
simulated in ECI with drag against the co-rotating air and gravity, observed
from three stations and solved with Trajectory), derotating with v_init gave
a mean final-direction error of 0.13 deg over seven geometries, against 0.18
deg with v_avg_norot. The difference is within the solver's own radiant error
for slow, long fireballs, and negligible for fast ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The solver only models the gravity drop when gravity_correction is set; the
fitted radiant is then the path tangent at its beginning. Without it, the
straight-line fit absorbs part of the curvature and is closer to the tangent
at the middle of the observed path, so integrating the turn from the
beginning would count part of it twice. The turn now starts at t = 0 or at the
middle of the observations accordingly.

End-to-end test with the full solver (including the timing re-run where the
gravity drop is applied), seven synthetic fireballs with drag against the
co-rotating air and gravity: mean final-direction error 0.081 deg with
gravity_correction (v_init derotation, turn from t = 0; 0.071 deg at the
start, i.e. the solver's own radiant error), and 0.102 deg without it (turn
from the middle, against 0.130 deg from t = 0). v_avg_norot gave 0.110 and
0.120 deg, supporting the previous commit's use of v_init.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y model

The evaluation height came from a straight line fitted to the measured
heights versus time. At the centre of the window that line is biased upwards
by the curvature of the decelerating path, and it carries the measurement
noise. computeFragEndParams() then mapped that height back to a time through
the solver's model heights, so the simulation started at a different instant
than the velocity it was given.

The height is now taken at the evaluation time from the same model
interpolation computeFragEndParams() uses (which includes the gravity drop),
so the height, velocity and time of the starting point coincide exactly.

Synthetic fireballs solved with the full solver: the error of the evaluation
height against the true height dropped from +522..+780 m to -19..+37 m for
a 15 km window, and from a mean of 338 m to 51 m over all tested cases. In
short windows at the very end of slow, long fireballs the error is bounded by
the solver's own model heights there (up to ~130 m).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The final point was placed on the fitted straight line, without the gravity
drop that the solver applies to its model points (and that the evaluation
height now includes), so the fragment started on the curved trajectory and
ended on the straight line. When gravity_correction is set, the final point
now gets the solver's drop at its time, keeping only the component
perpendicular to the line, as in the solver's model heights (its re-projection
onto the lines of sight absorbs the along-track part into the length).

Checks: at the observed points of real trajectories this reproduces the
solver's model heights to <= 10 m in 6 of 7 cases (56 m for a near-vertical
one), against up to 195 m without the drop and 118 m with the full vertical
drop. Against the true final position of synthetic fireballs solved with the
full solver, the mean height error dropped from 319 m to 201 m. The residual
comes from the solver's drop model itself, which assumes a constant vertical
velocity and overestimates the drop at the end of slow, long fireballs; its
own model heights show the same error there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the hardcoded 9.81 m/s^2 with MetSim's G0 scaled to the final
height, G0/(1 + h/r_earth)^2, where most of the turn accumulates (the turn
rate goes as 1/v). The effect is below 1% of the turn (~0.001 deg in a test
case); it keeps the constant consistent with the simulation used in the same
function. Also fixes a missing space.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The length came from interpolating height against length, which mixes the
savgol-smoothed heights with the raw lengths. At shallow entry angles a small
height difference becomes a large length difference, so the starting length
of the fragment, and with it the final point, could be off by hundreds of
metres, or kilometres for grazing trajectories.

It is now obtained by inverting the trajectory geometry: the length whose
point on the fitted line, with the solver's gravity drop, has the evaluation
height. The drop moves into pointOnTrajectory(), which is now used both here
and for the final point.

Checks on seven real trajectories: the geometric length reproduces the
solver's own stored lengths at every observed point (median ~0, rms <= 35 m),
the inversion recovers the evaluation height to 1e-9 m, and it differs from
the interpolated length by -78 to -2130 m near the evaluation region.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant