docs: bilingual comments for the access finder and pass summary

This commit is contained in:
ApsisRS contributors 2026-10-02 01:25:53 +08:00
parent 5857c44492
commit 295ba16d0d
2 changed files with 165 additions and 0 deletions

View File

@ -11,6 +11,11 @@ use apsis_math::Vec3;
/// in seconds from the caller's origin. The caller supplies a sequence of
/// these, which decouples the finder from the propagation and the frame
/// transformation.
///
/// 卫星轨迹的一个样本,按其被查找器消费的形式。
///
/// 位置在地固系中,单位为千米;时间以距调用方原点的秒数表示。调用方
/// 提供这样的一串样本,使查找器与传播及坐标系变换解耦。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OrbitSample {
/// The time of the sample, in seconds from the caller's origin.
@ -18,11 +23,15 @@ pub struct OrbitSample {
/// 样本时刻,单位为自调用方原点起的秒数。
pub seconds: f64,
/// The satellite position in the Earth-fixed frame, in kilometers.
///
/// 地固系中的卫星位置,单位为千米。
pub position_ecef_km: Vec3,
}
impl OrbitSample {
/// Creates a sample from a time and a position.
///
/// 由时间与位置创建样本。
pub const fn new(seconds: f64, position_ecef_km: Vec3) -> Self {
Self {
seconds,
@ -31,6 +40,8 @@ impl OrbitSample {
}
/// Returns `true` when the sample is usable.
///
/// 当该样本可用时返回 `true`。
pub fn is_valid(&self) -> bool {
self.seconds.is_finite() && self.position_ecef_km.is_finite()
}
@ -41,25 +52,43 @@ impl OrbitSample {
/// The finder holds the station and the numerical settings of the search.
/// The settings are separated from the search so that the same finder can be
/// applied to many satellites.
///
/// 为一个地面站查找可见性窗口。
///
/// 查找器保存地面站与搜索的数值设置。设置与搜索分开,使同一个查找器可
/// 应用于许多卫星。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AccessFinder {
/// The station and its elevation mask.
///
/// 地面站及其仰角遮蔽。
pub station: GroundPoint,
/// The interval between samples of the satellite track, in seconds.
///
/// A smaller step finds shorter passes but costs proportionally more
/// evaluations. A step of one minute resolves any pass a low Earth
/// satellite makes, since such a pass lasts several minutes.
///
/// 卫星轨迹样本之间的间隔,单位为秒。
///
/// 更小的步长能发现更短的过境,但求值次数成比例增加。一分钟的步长
/// 能分辨低轨卫星的任何过境,因为这样的过境持续数分钟。
pub sample_step_seconds: f64,
/// The tolerance on a crossing time, in seconds.
///
/// The refinement of a rise or set continues until the interval
/// containing the crossing is this narrow.
///
/// 穿越时刻的容差,单位为秒。
///
/// 对升起或降落的细化持续到包含该穿越的区间窄于该值为止。
pub crossing_tolerance_seconds: f64,
}
impl AccessFinder {
/// Returns `true` when the finder settings are usable.
///
/// 当查找器设置可用时返回 `true`。
pub fn is_valid(&self) -> bool {
self.station.is_valid()
&& self.sample_step_seconds.is_finite()
@ -71,6 +100,8 @@ impl AccessFinder {
/// Validates the settings, returning the reason when they are not
/// usable.
///
/// 校验设置,不可用时返回原因。
fn validate(&self) -> Result<(), AccessError> {
if !self.station.is_valid() {
return Err(AccessError::new(AccessErrorKind::InvalidStation));
@ -93,6 +124,10 @@ impl AccessFinder {
/// Returns the elevation of a sample, in radians.
///
/// Returns `None` when the sample or the station is not usable.
///
/// 返回某样本的仰角,单位为弧度。
///
/// 样本或地面站不可用时返回 `None`。
pub fn elevation_of(&self, sample: &OrbitSample) -> Option<f64> {
if !sample.is_valid() {
return None;
@ -129,11 +164,34 @@ impl AccessFinder {
/// checked rather than assumed, because the interpolation between two
/// samples binary searches the sequence and a binary search over an
/// unordered list returns a wrong index without failing.
///
/// 在有序的样本序列中查找可见性窗口。
///
/// 各样本必须按时间排序并覆盖所关注的区间。查找器假定仰角在样本
/// 之间连续,故步长必须短到卫星不会在一个步长内升起又降落。低轨
/// 卫星在十度遮蔽以上持续数分钟,故一分钟或更短的步长即满足。
///
/// 穿越时刻通过在跨越该穿越的两个样本之间线性插值卫星位置求得。
/// 两样本之间的弦与真实弧的偏差为步长的二阶量,故十秒步长给出的
/// 穿越精度约为十分之一秒,一秒步长约为毫秒。当传播器可用且希望
/// 穿越达到完整容差时,用 [`AccessFinder::find_with`]。
///
/// 在第一个样本处已进行中的过境,其升起时刻报告为第一个样本时间;
/// 在最后一个样本处仍在进行中的过境,其降落时刻报告为最后一个
/// 样本时间。该约定意味着过境被裁剪到所搜索的区间,而不外推超出
/// 它。
///
/// 设置不可用、样本序列少于两项、某样本不可用,或序列未按时间
/// 递增排列时返回错误。次序是被检查而非假定的,因为两样本之间的
/// 插值会对序列做二分查找,而对无序列表的二分查找会返回错误的下标
/// 而不报错。
pub fn find(&self, samples: &[OrbitSample]) -> Result<PassSummary, AccessError> {
self.find_internal(samples, |seconds| {
// Interpolate the position between the bracketing samples. The
// order of the sequence is checked by `find_internal` before
// this closure runs, and the step is the caller's to choose.
// 在夹逼的两个样本之间插值位置。序列的次序在该闭包运行前由
// `find_internal` 检查,步长由调用方选择。
let index = match samples.binary_search_by(|sample| {
sample
.seconds
@ -164,6 +222,11 @@ impl AccessFinder {
/// The function must be defined over the whole interval and must be
/// continuous, and the sample sequence must be ordered by time. The
/// closure returns `None` when the position at that time is not usable.
///
/// 在给定返回某时刻仰角的函数下运行搜索。
///
/// 该函数必须定义在整个区间上且必须连续,样本序列必须按时间排序。
/// 当该时刻的位置不可用时,该闭包返回 `None`。
fn find_internal<F>(
&self,
samples: &[OrbitSample],
@ -185,10 +248,17 @@ impl AccessFinder {
// crossing against the wrong pair of positions and report a pass that
// never happened. Checking only the ends, as this once did, let that
// through silently.
// 整个序列必须严格按时间递增,而不只是其两端。两样本之间的插值
// 会对该列表做二分查找,而对无序列表的二分查找会返回错误的下标
// 而不报错,故无序序列会针对错误的位置对细化穿越,并报告一个
// 从未发生过的过境。只检查两端(如本代码过去所做的)会使其无声
// 通过。
for (index, pair) in samples.windows(2).enumerate() {
// The test is written out rather than as `!(a < b)` so that it
// also rejects a pair whose times are not finite, and so that a
// reader can see that equality and disorder are both refused.
// 该测试是写开的而非写成 `!(a < b)`,使它也拒绝时间非有限的
// 点对,并使读者能看出相等与失序都被拒绝。
let ordered = pair[0].seconds.is_finite()
&& pair[1].seconds.is_finite()
&& pair[0].seconds < pair[1].seconds;
@ -229,6 +299,8 @@ impl AccessFinder {
let mut rise: Option<f64> = None;
// Track the highest elevation seen inside the current pass so that
// the peak is known without a second pass over the samples.
// 跟踪当前过境内所见的最大仰角,使峰值无需再遍历一遍样本即可
// 得知。
let mut peak = f64::NEG_INFINITY;
let mut peak_seconds = first;
let mut peak_position = samples[0].position_ecef_km;
@ -240,6 +312,8 @@ impl AccessFinder {
rise = Some(if index == 0 {
// The pass was already open at the start of the
// window, so it is clipped to the first sample.
// 该过境在窗口开始时就已打开,故被裁剪到第一个
// 样本。
samples[0].seconds
} else {
self.refine_crossing(samples, index, true, &mut elevation_at)
@ -266,6 +340,7 @@ impl AccessFinder {
// A pass still open at the end of the sequence is clipped to the
// last sample.
// 在序列末尾仍打开的过境被裁剪到最后一个样本。
if let Some(rise_seconds) = rise {
passes.push(Pass {
rise_seconds,
@ -290,6 +365,14 @@ impl AccessFinder {
/// increases through the mask; otherwise it is a set. The flag selects
/// which side of the crossing is above the mask, which keeps the search
/// bracket correct.
///
/// 细化仰角穿越遮蔽的时刻。
///
/// 该穿越位于下标 `index` 处的样本与其前一个之间,两者夹逼遮蔽。
/// 二分法把区间减半直到它窄于容差。
///
/// 当 `rising` 为真时该穿越是升起,即仰角递增地穿过遮蔽;否则是
/// 降落。该标志选择穿越的哪一侧在遮蔽之上,从而保持搜索夹逼正确。
fn refine_crossing<F>(
&self,
samples: &[OrbitSample],
@ -306,6 +389,8 @@ impl AccessFinder {
// The elevation difference from the mask, with the sign arranged so
// that the function is negative at `low` and positive at `high`.
// 相对遮蔽的仰角差,其符号被安排为使该函数在 `low` 处为负、在
// `high` 处为正。
let sign = if rising { 1.0 } else { -1.0 };
let mut difference = |seconds: f64| {
let elevation = elevation_at(seconds).unwrap_or(mask);
@ -342,6 +427,13 @@ impl AccessFinder {
///
/// Returns an error when the settings are not usable, when the interval
/// is not ordered, or when the propagator fails at a sample.
///
/// 在某个区间上查找可见性窗口,对每个样本调用一次传播器。
///
/// 传播器接收以距搜索原点的秒数表示的时间,返回地固系中的卫星
/// 位置。这使查找器可适配任何轨道来源,而本 crate 不依赖于其一。
///
/// 设置不可用、区间次序不对,或传播器在某样本处失败时返回错误。
pub fn find_with<F>(
&self,
start_seconds: f64,
@ -361,6 +453,7 @@ impl AccessFinder {
}
// Build the sample sequence, including the end point.
// 构建样本序列,包含端点。
let step = self.sample_step_seconds;
let count = ((end_seconds - start_seconds) / step).ceil() as usize + 1;
let mut samples = Vec::with_capacity(count);
@ -375,6 +468,8 @@ impl AccessFinder {
// The loop above may append the end point twice when the interval is
// an exact multiple of the step, so the last sample is replaced when
// it duplicates the end.
// 当区间是步长的精确整数倍时,上面的循环可能把端点追加两次,故
// 当最后一个样本与端点重复时将其替换。
if let Some(last) = samples.last_mut() {
if last.seconds != end_seconds {
let position = propagator(end_seconds).ok_or_else(|| {
@ -390,6 +485,8 @@ impl AccessFinder {
// The refinement calls the propagator at each bisection point, so
// the crossing is found to the tolerance rather than to the
// accuracy of a chord between two samples.
// 细化在每个二分点调用传播器,故穿越是按容差求得,而非按两样本
// 之间弦的精度求得。
let mut failure = None;
let summary = self.find_internal(&samples, |seconds| match propagator(seconds) {
Some(position) if position.is_finite() => elevation_angle(position, &self.station),

View File

@ -8,34 +8,58 @@ use apsis_math::Vec3;
/// Times are seconds from an origin chosen by the caller, so the same
/// structure serves a search over a scenario clock and one over elapsed
/// time.
///
/// 卫星高于某地面站仰角遮蔽的一个连续区间。
///
/// 时间是以距调用方所选原点的秒数表示,故同一结构既服务于按场景时钟的
/// 搜索,也服务于按流逝时间的搜索。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Pass {
/// The time the satellite rises above the mask, in seconds from the
/// caller's origin.
///
/// 卫星升到遮蔽之上的时间,以距调用方原点的秒数表示。
pub rise_seconds: f64,
/// The time the satellite falls below the mask.
///
/// 卫星降到遮蔽以下的时间。
pub set_seconds: f64,
/// The time of greatest elevation.
///
/// 仰角最大时的时间。
pub peak_seconds: f64,
/// The greatest elevation during the pass, in radians.
///
/// 该过境期间的最大仰角,单位为弧度。
pub peak_elevation: f64,
/// The elevation at which the satellite crossed the mask, in radians.
///
/// This is the station's mask, recorded so that a pass carries the
/// geometry it was found under.
///
/// 卫星穿越遮蔽时的仰角,单位为弧度。
///
/// 这是地面站的遮蔽,记录它是为了使过境携带其被发现时所依据的
/// 几何。
pub mask_elevation: f64,
/// The satellite position in the Earth-fixed frame at the peak, in
/// kilometers.
///
/// 峰值处地固系中的卫星位置,单位为千米。
pub peak_position: Vec3,
}
impl Pass {
/// Returns the duration of the pass, in seconds.
///
/// 返回该过境的时长,单位为秒。
pub fn duration(&self) -> f64 {
self.set_seconds - self.rise_seconds
}
/// Returns the time from the rise to the peak, in seconds.
///
/// 返回从升起到峰值的时间,单位为秒。
pub fn rise_to_peak(&self) -> f64 {
self.peak_seconds - self.rise_seconds
}
@ -46,6 +70,11 @@ impl Pass {
/// The pass stores only the peak, so this helper reports the stored
/// values at the three defining times and defers to the caller's
/// elevation function otherwise.
///
/// 返回过境内某时刻的仰角,当调用方能提供它时。
///
/// 过境只保存峰值,故该辅助函数在三个界定时刻报告所存的值,其余
/// 情形交由调用方的仰角函数。
pub fn contains(&self, seconds: f64) -> bool {
seconds >= self.rise_seconds && seconds <= self.set_seconds
}
@ -54,22 +83,34 @@ impl Pass {
///
/// This is the same as the peak elevation, named for readability at a
/// call site that compares passes.
///
/// 返回该过境的最大仰角,单位为弧度。
///
/// 这与峰值仰角相同,其命名是为在比较过境的调用处更具可读性。
pub fn maximum_elevation(&self) -> f64 {
self.peak_elevation
}
}
/// A summary of the access windows over an interval.
///
/// 某区间上可见性窗口的汇总。
#[derive(Debug, Clone, PartialEq)]
pub struct PassSummary {
/// The passes, in time order.
///
/// 按时序排列的各过境。
pub passes: Vec<Pass>,
/// The length of the interval searched, in seconds.
///
/// 所搜索区间的长度,单位为秒。
pub interval_seconds: f64,
}
impl PassSummary {
/// Creates a summary from a list of passes and the interval length.
///
/// 由过境列表与区间长度创建汇总。
pub fn new(passes: Vec<Pass>, interval_seconds: f64) -> Self {
Self {
passes,
@ -78,6 +119,8 @@ impl PassSummary {
}
/// Returns the number of passes.
///
/// 返回过境的数量。
pub fn count(&self) -> usize {
self.passes.len()
}
@ -85,6 +128,10 @@ impl PassSummary {
/// Returns the total access time, in seconds.
///
/// The passes are disjoint, so the durations add.
///
/// 返回总可见时间,单位为秒。
///
/// 各过境互不相交,故时长相加。
pub fn total_access_seconds(&self) -> f64 {
self.passes.iter().map(Pass::duration).sum()
}
@ -93,6 +140,10 @@ impl PassSummary {
/// accessible.
///
/// Returns `None` when the interval length is not positive.
///
/// 返回卫星可见所占该区间的比例。
///
/// 区间长度非正时返回 `None`。
pub fn access_fraction(&self) -> Option<f64> {
if self.interval_seconds > 0.0 && self.interval_seconds.is_finite() {
Some(self.total_access_seconds() / self.interval_seconds)
@ -104,6 +155,10 @@ impl PassSummary {
/// Returns the mean pass duration, in seconds.
///
/// Returns `None` when there are no passes.
///
/// 返回过境的平均时长,单位为秒。
///
/// 没有过境时返回 `None`。
pub fn mean_duration_seconds(&self) -> Option<f64> {
if self.passes.is_empty() {
return None;
@ -112,6 +167,8 @@ impl PassSummary {
}
/// Returns the longest pass, if there is one.
///
/// 返回最长的过境(若存在)。
pub fn longest(&self) -> Option<&Pass> {
self.passes.iter().max_by(|first, second| {
first
@ -122,6 +179,8 @@ impl PassSummary {
}
/// Returns the highest pass, if there is one.
///
/// 返回仰角最高的过境(若存在)。
pub fn highest(&self) -> Option<&Pass> {
self.passes.iter().max_by(|first, second| {
first
@ -135,6 +194,10 @@ impl PassSummary {
///
/// Returns `None` when either index is out of range or the two indices
/// are not consecutive and increasing.
///
/// 返回两个相邻过境之间的间隙,单位为秒。
///
/// 任一下标越界,或两个下标不相邻且递增时返回 `None`。
pub fn gap_seconds(&self, first: usize, second: usize) -> Option<f64> {
if second != first + 1 {
return None;
@ -148,6 +211,11 @@ impl PassSummary {
///
/// This is the longest time a station waits between contacts.
/// Returns `None` when there are fewer than two passes.
///
/// 返回相邻过境之间的最大间隙,单位为秒。
///
/// 这是一个地面站在两次通信之间等待的最长时间。过境少于两个时返回
/// `None`。
pub fn longest_gap_seconds(&self) -> Option<f64> {
if self.passes.len() < 2 {
return None;