From 295ba16d0dce80727345c77c75e5f168a8e1082b Mon Sep 17 00:00:00 2001 From: ApsisRS contributors Date: Fri, 2 Oct 2026 01:25:53 +0800 Subject: [PATCH] docs: bilingual comments for the access finder and pass summary --- crates/network/apsis-access/src/finder.rs | 97 +++++++++++++++++++++++ crates/network/apsis-access/src/pass.rs | 68 ++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/crates/network/apsis-access/src/finder.rs b/crates/network/apsis-access/src/finder.rs index 5d4f33f..def38a8 100644 --- a/crates/network/apsis-access/src/finder.rs +++ b/crates/network/apsis-access/src/finder.rs @@ -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 { 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 { 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( &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 = 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( &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( &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), diff --git a/crates/network/apsis-access/src/pass.rs b/crates/network/apsis-access/src/pass.rs index c94c0eb..e9e7083 100644 --- a/crates/network/apsis-access/src/pass.rs +++ b/crates/network/apsis-access/src/pass.rs @@ -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, /// 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, 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 { 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 { 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 { 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 { if self.passes.len() < 2 { return None;