access: search a constellation's tracks across several threads
A ground station's access schedule is one search per satellite, each independent of the others, and a shell gives enough of them to pay for the threads. The parallel form returns the same summaries in the same order as the sequential one, including an error at the position the sequential form would put it, so one bad track does not hide the rest.
This commit is contained in:
parent
d703704680
commit
e6ed78bdb0
|
|
@ -9,6 +9,8 @@ dependencies = [
|
||||||
"apsis-coverage",
|
"apsis-coverage",
|
||||||
"apsis-geodesy",
|
"apsis-geodesy",
|
||||||
"apsis-math",
|
"apsis-math",
|
||||||
|
"apsis-sgp4",
|
||||||
|
"apsis-tle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,11 @@ workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
apsis-math.workspace = true
|
apsis-math.workspace = true
|
||||||
apsis-geodesy.workspace = true
|
apsis-geodesy.workspace = true
|
||||||
|
apsis-tle.workspace = true
|
||||||
apsis-coverage.workspace = true
|
apsis-coverage.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
apsis-sgp4.workspace = true
|
||||||
|
apsis-coverage.workspace = true
|
||||||
|
apsis-geodesy.workspace = true
|
||||||
|
apsis-tle.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
//! Searching access windows for many tracks, optionally across several
|
||||||
|
//! threads.
|
||||||
|
//!
|
||||||
|
//! A ground station sees a whole constellation, so the access schedule is
|
||||||
|
//! the search of one track per satellite. Each track is independent of the
|
||||||
|
//! others, which makes the search the natural place to use several cores: the
|
||||||
|
//! work is proportional to the number of satellites, and a shell of a few
|
||||||
|
//! hundred gives enough of it to be worth the threads.
|
||||||
|
//!
|
||||||
|
//! The parallel entry point returns exactly what the sequential one does, in
|
||||||
|
//! the same order, so a caller can switch between them without changing what
|
||||||
|
//! it computes.
|
||||||
|
|
||||||
|
use crate::{AccessError, AccessFinder, OrbitSample, PassSummary};
|
||||||
|
use apsis_math::parallel::map_index;
|
||||||
|
|
||||||
|
/// Searches access for many tracks, in order.
|
||||||
|
///
|
||||||
|
/// The sequential form, provided for comparison and for small sets where
|
||||||
|
/// starting a thread would cost more than the search.
|
||||||
|
pub fn find_all(
|
||||||
|
finder: &AccessFinder,
|
||||||
|
tracks: &[Vec<OrbitSample>],
|
||||||
|
) -> Vec<Result<PassSummary, AccessError>> {
|
||||||
|
tracks.iter().map(|track| finder.find(track)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches access for many tracks, across several threads.
|
||||||
|
///
|
||||||
|
/// The result matches [`find_all`] element for element and in the same order.
|
||||||
|
/// A track that cannot be searched yields its error in the same position the
|
||||||
|
/// sequential form would put it, so one bad track does not hide the rest.
|
||||||
|
pub fn find_all_parallel(
|
||||||
|
finder: &AccessFinder,
|
||||||
|
tracks: &[Vec<OrbitSample>],
|
||||||
|
) -> Vec<Result<PassSummary, AccessError>> {
|
||||||
|
map_index(tracks.len(), |index| finder.find(&tracks[index]))
|
||||||
|
}
|
||||||
|
|
@ -34,10 +34,12 @@
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
mod batch;
|
||||||
mod error;
|
mod error;
|
||||||
mod finder;
|
mod finder;
|
||||||
mod pass;
|
mod pass;
|
||||||
|
|
||||||
|
pub use batch::{find_all, find_all_parallel};
|
||||||
pub use error::{AccessError, AccessErrorKind};
|
pub use error::{AccessError, AccessErrorKind};
|
||||||
pub use finder::{AccessFinder, OrbitSample};
|
pub use finder::{AccessFinder, OrbitSample};
|
||||||
pub use pass::{Pass, PassSummary};
|
pub use pass::{Pass, PassSummary};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
//! Tests for the batch access search.
|
||||||
|
//!
|
||||||
|
//! The parallel form must produce the same pass summaries, in the same
|
||||||
|
//! order, as the sequential one. Every test compares the two rather than
|
||||||
|
//! against a constant, so an ordering or content difference is caught.
|
||||||
|
|
||||||
|
use apsis_access::{AccessFinder, OrbitSample, find_all, find_all_parallel};
|
||||||
|
use apsis_coverage::GroundPoint;
|
||||||
|
use apsis_geodesy::Geodetic;
|
||||||
|
use apsis_math::Vec3;
|
||||||
|
use apsis_sgp4::{GravityModel, Satellite};
|
||||||
|
use apsis_tle::TwoLineElement;
|
||||||
|
|
||||||
|
const SAMPLE: &str = concat!(
|
||||||
|
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
|
||||||
|
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Returns a finder for a station on the equator.
|
||||||
|
fn finder() -> AccessFinder {
|
||||||
|
AccessFinder {
|
||||||
|
station: GroundPoint::new(Geodetic::new(0.0, 0.0, 0.0), 10.0_f64.to_radians()),
|
||||||
|
sample_step_seconds: 60.0,
|
||||||
|
crossing_tolerance_seconds: 0.01,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a track of Earth-fixed samples for one satellite.
|
||||||
|
///
|
||||||
|
/// The rotation is folded into a simple sweep so the track is a plausible
|
||||||
|
/// ground track; the exact geometry is not what this test is about, only
|
||||||
|
/// that the batch search reproduces the sequential one.
|
||||||
|
fn track(satellite: &Satellite, samples: usize) -> Vec<OrbitSample> {
|
||||||
|
(0..samples)
|
||||||
|
.map(|index| {
|
||||||
|
let seconds = index as f64 * 60.0;
|
||||||
|
let state = satellite
|
||||||
|
.propagate_minutes(seconds / 60.0)
|
||||||
|
.expect("the track propagates");
|
||||||
|
// A slow sweep in longitude with a constant latitude stands in
|
||||||
|
// for the rotation into the Earth-fixed frame.
|
||||||
|
let angle = seconds / 4_000.0;
|
||||||
|
let radius = 6_900.0;
|
||||||
|
let position = Vec3::new(radius * angle.cos(), radius * angle.sin(), radius * 0.3);
|
||||||
|
OrbitSample::new(seconds, position)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn satellite() -> Satellite {
|
||||||
|
let element = TwoLineElement::parse(SAMPLE).expect("the sample parses");
|
||||||
|
Satellite::from_element(&element, GravityModel::WGS72).expect("the orbit propagates")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_single_track_matches_the_sequential_form() {
|
||||||
|
let finder = finder();
|
||||||
|
let tracks = vec![track(&satellite(), 120)];
|
||||||
|
assert_eq!(
|
||||||
|
find_all_parallel(&finder, &tracks),
|
||||||
|
find_all(&finder, &tracks)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_shell_of_tracks_matches_the_sequential_form() {
|
||||||
|
// Long enough that the parallel path takes the multi thread branch.
|
||||||
|
let finder = finder();
|
||||||
|
let satellite = satellite();
|
||||||
|
let tracks: Vec<Vec<OrbitSample>> = (0..200).map(|_| track(&satellite, 120)).collect();
|
||||||
|
assert_eq!(
|
||||||
|
find_all_parallel(&finder, &tracks),
|
||||||
|
find_all(&finder, &tracks)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_empty_set_of_tracks_is_empty() {
|
||||||
|
let finder = finder();
|
||||||
|
assert!(find_all_parallel(&finder, &[]).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bad_track_does_not_hide_the_others() {
|
||||||
|
// A track that is too short or out of order yields an error in both
|
||||||
|
// forms, at the same position, and the good tracks are still searched.
|
||||||
|
let finder = finder();
|
||||||
|
let satellite = satellite();
|
||||||
|
let mut tracks = vec![track(&satellite, 120), vec![], track(&satellite, 120)];
|
||||||
|
// The empty track is a degenerate case; search it as given.
|
||||||
|
let sequential = find_all(&finder, &tracks);
|
||||||
|
let parallel = find_all_parallel(&finder, &tracks);
|
||||||
|
assert_eq!(parallel, sequential);
|
||||||
|
tracks.clear();
|
||||||
|
assert!(tracks.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_result_is_in_input_order() {
|
||||||
|
// Three tracks with different lengths, so each summary differs and the
|
||||||
|
// ordering is observable.
|
||||||
|
let finder = finder();
|
||||||
|
let satellite = satellite();
|
||||||
|
let tracks = vec![
|
||||||
|
track(&satellite, 40),
|
||||||
|
track(&satellite, 120),
|
||||||
|
track(&satellite, 80),
|
||||||
|
];
|
||||||
|
let parallel = find_all_parallel(&finder, &tracks);
|
||||||
|
assert_eq!(parallel.len(), 3);
|
||||||
|
for (index, summary) in parallel.iter().enumerate() {
|
||||||
|
let expected = finder.find(&tracks[index]).expect("the track is valid");
|
||||||
|
assert_eq!(summary.as_ref().expect("the track is valid"), &expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue