GroundStation in src/station.rs: a name, a Geodetic location and the lowest
elevation it will observe at, defaulting to five degrees. Zero is the geometric horizon,
which no real station reaches — the atmosphere is thickest there, the ground is usually in
the way, and a refraction correction stops being trustworthy.
The location is f64 rather than the propagated scalar, because a station does not move
with the state and nothing differentiates with respect to where it is. The target does, so elevation is generic over the scalar and a measurement partial falls out of it.
Most of this was already present: Geodetic::east_north_up and Geodetic::look do the
geometry, and this adds the mask and the visibility test. The surface is look, returning
the east-north-up vector, plus elevation and sees.
Scope boundary with #14. Azimuth, range, range-rate and topocentric RA/DEC are
deliberately not here, even though azimuth and range are two lines each off the
east-north-up vector. #14 is where the MeasurementModel trait lives, and it should build
on that vector rather than keep a second copy of the geometry in step with this one.
The test worth pointing at is test_the_vertical_is_the_normal_not_the_radius. The
local vertical is the ellipsoid normal, which on an oblate body does not point at the
center, so a target straight out along the geocentric radius is not overhead: it misses by
the deflection between geodetic and geocentric latitude, about a fifth of a degree at this
station. The test derives that deflection from the station's own position rather than
hardcoding it and asserts the elevation is pi/2 - deflection. Using geocentric latitude
anywhere in the path fails it. The equatorial case, where the deflection vanishes and the
radius really is the vertical, is tested next to it.
Passes are found by post-processing, not by a built-in event. A callback is needed to act at a boundary, not to find one: the solution carries dense output, so walking it
and bisecting for a crossing is enough, and that is the same shape as reading jump times
off a solution. The arguments that look like they favor an event do not hold — bisecting
the interpolant reaches machine precision, a pass shorter than one step is missed either
way because steps are sized by the dynamics rather than by station geometry, and event
location costs about four steps per crossing per station inside every run. What would
change it is something that alters the dynamics at a boundary, spacecraft controls being
the case in view.
A user who wants that today can already do it with no new API, and tests/ground_station_passes.rs is the worked example. It implements Callback on its own
struct, takes the epoch, frame and kernels off Assembly::context, rotates the state into
the body-fixed frame and returns elevation - mask. Over a day of a 600 km orbit it finds
14 crossings, seven passes of seven and a half to eleven minutes, and the elevation at each
located crossing is the mask to better than 1e-9 rad.
That test is an integration test rather than a unit one on purpose, and it imports from std and this crate and nothing else, so it is the same thing a consumer would write. It
turned up two gaps in the public surface, both now closed:
ODE was not re-exported. Callback was, but ODE appears in Callback::effect's own
signature, so the trait could not be implemented without depending on ordinary-diffeq
directly just to name a parameter type.
nalgebra is now re-exported at the crate root. Its types are throughout this API, so a
consumer needs them, and reaching them through here is what keeps the version matched.
Worth recording for anyone testing reachability this way: a test, example or bench target
gets the crate's own dependencies as well as its dev-dependencies, so it can reach straight
through to nalgebra or ordinary-diffeq whether or not they are re-exported. Neither gap
shows up unless the test deliberately imports from this crate alone.
Done, in `59e9ba3`.
`GroundStation` in `src/station.rs`: a name, a `Geodetic` location and the lowest
elevation it will observe at, defaulting to five degrees. Zero is the geometric horizon,
which no real station reaches — the atmosphere is thickest there, the ground is usually in
the way, and a refraction correction stops being trustworthy.
The location is `f64` rather than the propagated scalar, because a station does not move
with the state and nothing differentiates with respect to where it is. The target does, so
`elevation` is generic over the scalar and a measurement partial falls out of it.
Most of this was already present: `Geodetic::east_north_up` and `Geodetic::look` do the
geometry, and this adds the mask and the visibility test. The surface is `look`, returning
the east-north-up vector, plus `elevation` and `sees`.
**Scope boundary with #14.** Azimuth, range, range-rate and topocentric RA/DEC are
deliberately not here, even though azimuth and range are two lines each off the
east-north-up vector. #14 is where the `MeasurementModel` trait lives, and it should build
on that vector rather than keep a second copy of the geometry in step with this one.
**The test worth pointing at** is `test_the_vertical_is_the_normal_not_the_radius`. The
local vertical is the ellipsoid normal, which on an oblate body does not point at the
center, so a target straight out along the geocentric radius is not overhead: it misses by
the deflection between geodetic and geocentric latitude, about a fifth of a degree at this
station. The test derives that deflection from the station's own position rather than
hardcoding it and asserts the elevation is `pi/2 - deflection`. Using geocentric latitude
anywhere in the path fails it. The equatorial case, where the deflection vanishes and the
radius really is the vertical, is tested next to it.
**Passes are found by post-processing, not by a built-in event.** A callback is needed to
*act* at a boundary, not to *find* one: the solution carries dense output, so walking it
and bisecting for a crossing is enough, and that is the same shape as reading jump times
off a solution. The arguments that look like they favor an event do not hold — bisecting
the interpolant reaches machine precision, a pass shorter than one step is missed either
way because steps are sized by the dynamics rather than by station geometry, and event
location costs about four steps per crossing per station inside every run. What would
change it is something that alters the dynamics at a boundary, spacecraft controls being
the case in view.
A user who wants that today can already do it with no new API, and
`tests/ground_station_passes.rs` is the worked example. It implements `Callback` on its own
struct, takes the epoch, frame and kernels off `Assembly::context`, rotates the state into
the body-fixed frame and returns `elevation - mask`. Over a day of a 600 km orbit it finds
14 crossings, seven passes of seven and a half to eleven minutes, and the elevation at each
located crossing is the mask to better than 1e-9 rad.
That test is an integration test rather than a unit one on purpose, and it imports from
`std` and this crate and nothing else, so it is the same thing a consumer would write. It
turned up two gaps in the public surface, both now closed:
- `ODE` was not re-exported. `Callback` was, but `ODE` appears in `Callback::effect`'s own
signature, so the trait could not be implemented without depending on `ordinary-diffeq`
directly just to name a parameter type.
- `nalgebra` is now re-exported at the crate root. Its types are throughout this API, so a
consumer needs them, and reaching them through here is what keeps the version matched.
Worth recording for anyone testing reachability this way: a test, example or bench target
gets the crate's own dependencies as well as its dev-dependencies, so it can reach straight
through to `nalgebra` or `ordinary-diffeq` whether or not they are re-exported. Neither gap
shows up unless the test deliberately imports from this crate alone.
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.
Location, elevation mask, visibility.
M1.
Done, in
59e9ba3.GroundStationinsrc/station.rs: a name, aGeodeticlocation and the lowestelevation it will observe at, defaulting to five degrees. Zero is the geometric horizon,
which no real station reaches — the atmosphere is thickest there, the ground is usually in
the way, and a refraction correction stops being trustworthy.
The location is
f64rather than the propagated scalar, because a station does not movewith the state and nothing differentiates with respect to where it is. The target does, so
elevationis generic over the scalar and a measurement partial falls out of it.Most of this was already present:
Geodetic::east_north_upandGeodetic::lookdo thegeometry, and this adds the mask and the visibility test. The surface is
look, returningthe east-north-up vector, plus
elevationandsees.Scope boundary with #14. Azimuth, range, range-rate and topocentric RA/DEC are
deliberately not here, even though azimuth and range are two lines each off the
east-north-up vector. #14 is where the
MeasurementModeltrait lives, and it should buildon that vector rather than keep a second copy of the geometry in step with this one.
The test worth pointing at is
test_the_vertical_is_the_normal_not_the_radius. Thelocal vertical is the ellipsoid normal, which on an oblate body does not point at the
center, so a target straight out along the geocentric radius is not overhead: it misses by
the deflection between geodetic and geocentric latitude, about a fifth of a degree at this
station. The test derives that deflection from the station's own position rather than
hardcoding it and asserts the elevation is
pi/2 - deflection. Using geocentric latitudeanywhere in the path fails it. The equatorial case, where the deflection vanishes and the
radius really is the vertical, is tested next to it.
Passes are found by post-processing, not by a built-in event. A callback is needed to
act at a boundary, not to find one: the solution carries dense output, so walking it
and bisecting for a crossing is enough, and that is the same shape as reading jump times
off a solution. The arguments that look like they favor an event do not hold — bisecting
the interpolant reaches machine precision, a pass shorter than one step is missed either
way because steps are sized by the dynamics rather than by station geometry, and event
location costs about four steps per crossing per station inside every run. What would
change it is something that alters the dynamics at a boundary, spacecraft controls being
the case in view.
A user who wants that today can already do it with no new API, and
tests/ground_station_passes.rsis the worked example. It implementsCallbackon its ownstruct, takes the epoch, frame and kernels off
Assembly::context, rotates the state intothe body-fixed frame and returns
elevation - mask. Over a day of a 600 km orbit it finds14 crossings, seven passes of seven and a half to eleven minutes, and the elevation at each
located crossing is the mask to better than 1e-9 rad.
That test is an integration test rather than a unit one on purpose, and it imports from
stdand this crate and nothing else, so it is the same thing a consumer would write. Itturned up two gaps in the public surface, both now closed:
ODEwas not re-exported.Callbackwas, butODEappears inCallback::effect's ownsignature, so the trait could not be implemented without depending on
ordinary-diffeqdirectly just to name a parameter type.
nalgebrais now re-exported at the crate root. Its types are throughout this API, so aconsumer needs them, and reaching them through here is what keeps the version matched.
Worth recording for anyone testing reachability this way: a test, example or bench target
gets the crate's own dependencies as well as its dev-dependencies, so it can reach straight
through to
nalgebraorordinary-diffeqwhether or not they are re-exported. Neither gapshows up unless the test deliberately imports from this crate alone.