Range, range-rate, azimuth/elevation and topocentric RA/DEC behind a MeasurementModel trait, generic over the scalar so the filters get exact Jacobians. Light-time and aberration behind feature flags, off by default.
M1.
Range, range-rate, azimuth/elevation and topocentric RA/DEC behind a `MeasurementModel` trait, generic over the scalar so the filters get exact Jacobians. Light-time and aberration behind feature flags, off by default.
M1.
connor
added this to the M1 milestone 2026-09-30 11:24:57 -04:00
connor
added the size/M label 2026-09-30 11:24:57 -04:00
Done, in d334e07. Leaving it open for you to close.
src/measurement.rs, plus state_in, relative_to and east_north_up on GroundStation, and a new src/constants.rs.
An enum rather than a trait, which is a deliberate departure from the wording above.
A prediction has to come out in whatever scalar is being propagated, so the method is
generic, and a trait with a generic method is not object-safe — the same reason DensityModel and SurfaceModel are enums. Three shapes were considered: mirroring Dynamics/DynamicsModel with a serializable enum over an object-safe trait, a trait
carrying the measurement width as a const parameter the way Layout carries the state
width, or a plain enum. The plain enum won as the smallest surface. The cost, accepted
knowingly: a measurement cannot be added from outside the crate.
The variants are Range, RangeRate, AzimuthElevation, RangeAzimuthElevation and RightAscensionDeclination. The radar one was not in the original list and is a real
tracking data type; its three numbers share an observation time and a covariance, so it is
its own variant rather than something a caller assembles.
The geometry is free functions and the enum only selects among them.range and range_rate take a relative state, azimuth_elevation takes an east-north-up vector, right_ascension_declination takes a relative state in an inertial frame. Each takes the
one thing it needs, so none of them needs a station, an epoch or an almanac.
Observed is the geometry prepared once per epoch, since it costs two rotations. It
stores the relative state and a horizon rotation and derives the east-north-up vector
rather than storing it, so a correction applied to the relative state reaches the bearings
as well as the range. Storing both would have let them disagree the moment a correction
existed.
Light time and aberration are ordinary methods, not Cargo features
This reverses what the issue asks for, after the reasoning was gone through and agreed.
The issue text above is now wrong, so this is the record of why.
The likely original motive was that light time would be expensive: done properly it solves
for the retarded position against the target's trajectory, which would couple this module
to a propagation result. It is not. At orbital speeds the flight time is milliseconds and
the target moves tens of meters, so a first-order correction from the state alone is good
to well under a millimeter, and the whole thing is about fifteen lines.
Against the flags:
Cargo features are global and additive. One crate anywhere in the build graph enabling light-time turns it on for everyone, which makes a build-level switch govern a
per-measurement physical choice, and makes it impossible to compare a corrected arc
against an uncorrected one in the same program.
It breaks run provenance. A record has to say whether light time was applied; as a
feature that is build configuration rather than part of the recipe, so two runs of the
same recorded recipe could legitimately disagree.
Every feature doubles the build matrix. Running clippy at three settings caught a real
pre-existing warning; at eight that stops being done.
What a flag would have bought is zero cost when unused and a signal that something is
unvalidated. The cost is a method nobody calls, which is already zero, and the signal is a
doc comment. So with_light_time and with_aberration are always available and never
applied by Observed::new.
Measured
Range and range rate do not depend on which frame the relative state is in, which only
holds because the station's own motion is subtracted rather than ignored. The control
asserts the identity naive - range_rate == station_velocity . line_of_sight exactly,
rather than bounding it by a magnitude — the first attempt bounded it by the station's speed, about 380 m/s, and the actual error is that velocity projected on the line of
sight, 18.8 m/s for the geometry tested.
Light time moves the target by no more than it travels while the light is in flight,
bounded by target_speed * range / c rather than by a remembered number.
Aberration tilts the line of sight by no more than |v| / c and leaves the range
untouched; about a microradian for a station carried east at a few hundred m/s.
The radar measurement's three numbers are exactly what Range and AzimuthElevation
give separately.
228 unit tests, 1 integration, 7 doc. Clippy clean with default, all and no features.
Also in this commit
The physical constants moved out of the sunshine force model into src/constants.rs: LIGHT_SPEED, ASTRONOMICAL_UNIT and IRRADIANCE_AT_1AU. A force model is the wrong home
for a fact about the universe, and the measurement code needed the speed of light too. The
line drawn is that a constant goes there if it is a fact about the universe and stays with
its model if it is a parameter of that model, so Harris-Priester's bulge lag, the MRP
shadow threshold and the representation tolerances all stayed where they were. Each entry
says whether it is a definition or a measurement, because IRRADIANCE_AT_1AU moves about a
tenth of a percent over the solar cycle and that bounds what a radiation pressure result
can claim.
An unused Vector6 import in propagation.rs's tests, visible only under --no-default-features, was fixed in the same pass.
Known limitations
The enum is the wrong shape if composites multiply. Five variants is fine. A radar
that also reported range rate, or any other combination, would mean enumerating subsets,
and at that point a measurement should be a set of components rather than a name for each
combination.
Which corrections were applied is not yet recorded anywhere. That is what the feature
flags nominally provided, and dropping them is only safe once a run's record carries it.
Noted on #12.
No light-time iteration against the actual trajectory. Unnecessary at orbital speeds, as
above, but it would be needed for anything much further away.
Done, in `d334e07`. Leaving it open for you to close.
`src/measurement.rs`, plus `state_in`, `relative_to` and `east_north_up` on
`GroundStation`, and a new `src/constants.rs`.
**An enum rather than a trait**, which is a deliberate departure from the wording above.
A prediction has to come out in whatever scalar is being propagated, so the method is
generic, and a trait with a generic method is not object-safe — the same reason
`DensityModel` and `SurfaceModel` are enums. Three shapes were considered: mirroring
`Dynamics`/`DynamicsModel` with a serializable enum over an object-safe trait, a trait
carrying the measurement width as a const parameter the way `Layout` carries the state
width, or a plain enum. The plain enum won as the smallest surface. The cost, accepted
knowingly: a measurement cannot be added from outside the crate.
The variants are `Range`, `RangeRate`, `AzimuthElevation`, `RangeAzimuthElevation` and
`RightAscensionDeclination`. The radar one was not in the original list and is a real
tracking data type; its three numbers share an observation time and a covariance, so it is
its own variant rather than something a caller assembles.
**The geometry is free functions and the enum only selects among them.** `range` and
`range_rate` take a relative state, `azimuth_elevation` takes an east-north-up vector,
`right_ascension_declination` takes a relative state in an inertial frame. Each takes the
one thing it needs, so none of them needs a station, an epoch or an almanac.
`Observed` is the geometry prepared once per epoch, since it costs two rotations. It
stores the relative state and a horizon rotation and *derives* the east-north-up vector
rather than storing it, so a correction applied to the relative state reaches the bearings
as well as the range. Storing both would have let them disagree the moment a correction
existed.
### Light time and aberration are ordinary methods, not Cargo features
This reverses what the issue asks for, after the reasoning was gone through and agreed.
The issue text above is now wrong, so this is the record of why.
The likely original motive was that light time would be expensive: done properly it solves
for the retarded position against the target's *trajectory*, which would couple this module
to a propagation result. It is not. At orbital speeds the flight time is milliseconds and
the target moves tens of meters, so a first-order correction from the state alone is good
to well under a millimeter, and the whole thing is about fifteen lines.
Against the flags:
- Cargo features are global and additive. One crate anywhere in the build graph enabling
`light-time` turns it on for everyone, which makes a build-level switch govern a
per-measurement physical choice, and makes it impossible to compare a corrected arc
against an uncorrected one in the same program.
- It breaks run provenance. A record has to say whether light time was applied; as a
feature that is build configuration rather than part of the recipe, so two runs of the
same recorded recipe could legitimately disagree.
- Every feature doubles the build matrix. Running clippy at three settings caught a real
pre-existing warning; at eight that stops being done.
What a flag would have bought is zero cost when unused and a signal that something is
unvalidated. The cost is a method nobody calls, which is already zero, and the signal is a
doc comment. So `with_light_time` and `with_aberration` are always available and never
applied by `Observed::new`.
### Measured
- Range and range rate do not depend on which frame the relative state is in, which only
holds because the station's own motion is subtracted rather than ignored. The control
asserts the identity `naive - range_rate == station_velocity . line_of_sight` exactly,
rather than bounding it by a magnitude — the first attempt bounded it by the station's
*speed*, about 380 m/s, and the actual error is that velocity projected on the line of
sight, 18.8 m/s for the geometry tested.
- Light time moves the target by no more than it travels while the light is in flight,
bounded by `target_speed * range / c` rather than by a remembered number.
- Aberration tilts the line of sight by no more than `|v| / c` and leaves the range
untouched; about a microradian for a station carried east at a few hundred m/s.
- The radar measurement's three numbers are exactly what `Range` and `AzimuthElevation`
give separately.
228 unit tests, 1 integration, 7 doc. Clippy clean with default, all and no features.
### Also in this commit
The physical constants moved out of the sunshine force model into `src/constants.rs`:
`LIGHT_SPEED`, `ASTRONOMICAL_UNIT` and `IRRADIANCE_AT_1AU`. A force model is the wrong home
for a fact about the universe, and the measurement code needed the speed of light too. The
line drawn is that a constant goes there if it is a fact about the universe and stays with
its model if it is a parameter of that model, so Harris-Priester's bulge lag, the MRP
shadow threshold and the representation tolerances all stayed where they were. Each entry
says whether it is a definition or a measurement, because `IRRADIANCE_AT_1AU` moves about a
tenth of a percent over the solar cycle and that bounds what a radiation pressure result
can claim.
An unused `Vector6` import in `propagation.rs`'s tests, visible only under
`--no-default-features`, was fixed in the same pass.
### Known limitations
- **The enum is the wrong shape if composites multiply.** Five variants is fine. A radar
that also reported range rate, or any other combination, would mean enumerating subsets,
and at that point a measurement should be a set of components rather than a name for each
combination.
- **Which corrections were applied is not yet recorded anywhere.** That is what the feature
flags nominally provided, and dropping them is only safe once a run's record carries it.
Noted on #12.
- No light-time iteration against the actual trajectory. Unnecessary at orbital speeds, as
above, but it would be needed for anything much further away.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Range, range-rate, azimuth/elevation and topocentric RA/DEC behind a
MeasurementModeltrait, generic over the scalar so the filters get exact Jacobians. Light-time and aberration behind feature flags, off by default.M1.
Done, in
d334e07. Leaving it open for you to close.src/measurement.rs, plusstate_in,relative_toandeast_north_uponGroundStation, and a newsrc/constants.rs.An enum rather than a trait, which is a deliberate departure from the wording above.
A prediction has to come out in whatever scalar is being propagated, so the method is
generic, and a trait with a generic method is not object-safe — the same reason
DensityModelandSurfaceModelare enums. Three shapes were considered: mirroringDynamics/DynamicsModelwith a serializable enum over an object-safe trait, a traitcarrying the measurement width as a const parameter the way
Layoutcarries the statewidth, or a plain enum. The plain enum won as the smallest surface. The cost, accepted
knowingly: a measurement cannot be added from outside the crate.
The variants are
Range,RangeRate,AzimuthElevation,RangeAzimuthElevationandRightAscensionDeclination. The radar one was not in the original list and is a realtracking data type; its three numbers share an observation time and a covariance, so it is
its own variant rather than something a caller assembles.
The geometry is free functions and the enum only selects among them.
rangeandrange_ratetake a relative state,azimuth_elevationtakes an east-north-up vector,right_ascension_declinationtakes a relative state in an inertial frame. Each takes theone thing it needs, so none of them needs a station, an epoch or an almanac.
Observedis the geometry prepared once per epoch, since it costs two rotations. Itstores the relative state and a horizon rotation and derives the east-north-up vector
rather than storing it, so a correction applied to the relative state reaches the bearings
as well as the range. Storing both would have let them disagree the moment a correction
existed.
Light time and aberration are ordinary methods, not Cargo features
This reverses what the issue asks for, after the reasoning was gone through and agreed.
The issue text above is now wrong, so this is the record of why.
The likely original motive was that light time would be expensive: done properly it solves
for the retarded position against the target's trajectory, which would couple this module
to a propagation result. It is not. At orbital speeds the flight time is milliseconds and
the target moves tens of meters, so a first-order correction from the state alone is good
to well under a millimeter, and the whole thing is about fifteen lines.
Against the flags:
light-timeturns it on for everyone, which makes a build-level switch govern aper-measurement physical choice, and makes it impossible to compare a corrected arc
against an uncorrected one in the same program.
feature that is build configuration rather than part of the recipe, so two runs of the
same recorded recipe could legitimately disagree.
pre-existing warning; at eight that stops being done.
What a flag would have bought is zero cost when unused and a signal that something is
unvalidated. The cost is a method nobody calls, which is already zero, and the signal is a
doc comment. So
with_light_timeandwith_aberrationare always available and neverapplied by
Observed::new.Measured
holds because the station's own motion is subtracted rather than ignored. The control
asserts the identity
naive - range_rate == station_velocity . line_of_sightexactly,rather than bounding it by a magnitude — the first attempt bounded it by the station's
speed, about 380 m/s, and the actual error is that velocity projected on the line of
sight, 18.8 m/s for the geometry tested.
bounded by
target_speed * range / crather than by a remembered number.|v| / cand leaves the rangeuntouched; about a microradian for a station carried east at a few hundred m/s.
RangeandAzimuthElevationgive separately.
228 unit tests, 1 integration, 7 doc. Clippy clean with default, all and no features.
Also in this commit
The physical constants moved out of the sunshine force model into
src/constants.rs:LIGHT_SPEED,ASTRONOMICAL_UNITandIRRADIANCE_AT_1AU. A force model is the wrong homefor a fact about the universe, and the measurement code needed the speed of light too. The
line drawn is that a constant goes there if it is a fact about the universe and stays with
its model if it is a parameter of that model, so Harris-Priester's bulge lag, the MRP
shadow threshold and the representation tolerances all stayed where they were. Each entry
says whether it is a definition or a measurement, because
IRRADIANCE_AT_1AUmoves about atenth of a percent over the solar cycle and that bounds what a radiation pressure result
can claim.
An unused
Vector6import inpropagation.rs's tests, visible only under--no-default-features, was fixed in the same pass.Known limitations
that also reported range rate, or any other combination, would mean enumerating subsets,
and at that point a measurement should be a set of components rather than a name for each
combination.
flags nominally provided, and dropping them is only safe once a run's record carries it.
Noted on #12.
above, but it would be needed for anything much further away.