fix(sat): the Doppler correction was 250 times too big, and backwards
Reported as "the Doppler moves the frequency enormously", and it did: a 2 m
downlink was being shifted two megahertz across a pass instead of three
kilohertz, in the wrong direction.
The propagator library reports a range rate that is not one. Measured against
the range it is meant to be the derivative of, on this station's own cached
elements:
PO-101 library -1457.3 km/s measured +6.227 km/s
ISS library +2036.8 km/s measured -5.522 km/s
Wrong by a factor of some 250 and of the wrong sign, so the correction both
overshot and pushed the operator away from the station they could hear. Nothing
else was affected — the elevation and the passes come from the look angle, which
is right — which is why this survived: the satellite was in the correct place on
the map while the radio was told to go megahertz away from it.
So OpsLog computes it itself, as the difference between two ranges a second
apart. That cannot be wrong in either magnitude or sign: it differentiates the
very number the panel displays. Two extra propagations per call, which is
microseconds.
Two tests pin it, and both fail against the old behaviour: a range rate faster
than orbital velocity is a units mistake, and the Doppler on the two bands
satellites are worked on has a textbook size — about ±3.5 kHz on 2 m, ±10 kHz on
70 cm.
cmd/satdiag is the throwaway that found it, kept because the next report of this
shape ("the frequency moves oddly", "that pass is not real") is answered by the
same three numbers: which elements the satellite resolved to, the range rate
reported against the range rate measured, and the Doppler each transponder gets.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
+37
-4
@@ -271,9 +271,6 @@ func (e Element) Track(obs Observer, at time.Time) (Position, error) {
|
||||
if err != nil {
|
||||
return Position{}, fmt.Errorf("sat: %q: %w", e.Name, err)
|
||||
}
|
||||
// The state vector carries the position AND the velocity, which is what the
|
||||
// look angle needs for the range rate — and the range rate is the whole of
|
||||
// the Doppler shift.
|
||||
sv := &sgp4.StateVector{
|
||||
X: eci.Position.X, Y: eci.Position.Y, Z: eci.Position.Z,
|
||||
VX: eci.Velocity.X, VY: eci.Velocity.Y, VZ: eci.Velocity.Z,
|
||||
@@ -292,10 +289,46 @@ func (e Element) Track(obs Observer, at time.Time) (Position, error) {
|
||||
Az: o.LookAngles.Azimuth,
|
||||
El: o.LookAngles.Elevation,
|
||||
RangeKm: o.LookAngles.Range,
|
||||
RangeRate: o.LookAngles.RangeRate,
|
||||
RangeRate: e.rangeRate(loc, at.UTC()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// rangeRate is how fast the satellite is closing or opening, in km/s.
|
||||
//
|
||||
// MEASURED, not taken from the propagator. The library reports a range rate
|
||||
// that is wrong by a factor of some 250 AND has the wrong sign — the ISS at
|
||||
// −5.5 km/s (closing) came back as +2036 km/s — which put the Doppler
|
||||
// correction hundreds of kilohertz out and moved it the wrong way. The
|
||||
// difference between two ranges a second apart cannot be wrong in either
|
||||
// respect: it differentiates the very number the panel displays.
|
||||
//
|
||||
// Two extra propagations per call. SGP4 costs microseconds and this runs at
|
||||
// most a few hundred times a second across every satellite on screen, so the
|
||||
// price of being right here is not worth optimising away.
|
||||
func (e Element) rangeRate(loc *sgp4.Location, at time.Time) float64 {
|
||||
const dt = time.Second // ±1 s: far below any curvature in the range, far above float noise
|
||||
before, ok1 := e.rangeAt(loc, at.Add(-dt))
|
||||
after, ok2 := e.rangeAt(loc, at.Add(dt))
|
||||
if !ok1 || !ok2 {
|
||||
return 0
|
||||
}
|
||||
return (after - before) / (2 * dt.Seconds())
|
||||
}
|
||||
|
||||
// rangeAt is the distance to the satellite at one instant, in km.
|
||||
func (e Element) rangeAt(loc *sgp4.Location, at time.Time) (float64, bool) {
|
||||
eci, err := e.tle.FindPositionAtTime(at.UTC())
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
sv := &sgp4.StateVector{X: eci.Position.X, Y: eci.Position.Y, Z: eci.Position.Z}
|
||||
o, err := sv.GetLookAngle(loc, at.UTC())
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return o.LookAngles.Range, true
|
||||
}
|
||||
|
||||
// earthRadiusKm is the mean radius — the footprint is a circle drawn on a
|
||||
// sphere, and a metre of flattening does not show at that scale.
|
||||
const earthRadiusKm = 6371.0
|
||||
|
||||
Reference in New Issue
Block a user