diff --git a/crates/orbit/apsis-ephemeris/src/ephemeris.rs b/crates/orbit/apsis-ephemeris/src/ephemeris.rs index 7e6eda8..58650b6 100644 --- a/crates/orbit/apsis-ephemeris/src/ephemeris.rs +++ b/crates/orbit/apsis-ephemeris/src/ephemeris.rs @@ -8,18 +8,31 @@ use apsis_math::Vec3; /// A sample is a position and velocity at a time, which is the form every /// propagator in the workspace produces and the form the interpolation /// needs. +/// +/// 一个采样状态。 +/// +/// 样本是某时刻的位置与速度,这既是工作区中每个传播器产出的形式,也是插值所需 +/// 的形式。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct Sample { /// The time of the sample, in seconds from the caller's origin. + /// + /// 样本时刻,单位为自调用方原点起的秒数。 pub seconds: f64, /// Position, in kilometers. + /// + /// 位置,单位为千米。 pub position: Vec3, /// Velocity, in kilometers per second. + /// + /// 速度,单位为千米每秒。 pub velocity: Vec3, } impl Sample { /// Creates a sample. + /// + /// 创建一个样本。 pub const fn new(seconds: f64, position: Vec3, velocity: Vec3) -> Self { Self { seconds, @@ -29,16 +42,22 @@ impl Sample { } /// Returns `true` when the time, position, and velocity are finite. + /// + /// 当时间、位置与速度都有限时返回 `true`。 pub fn is_valid(&self) -> bool { self.seconds.is_finite() && self.position.is_finite() && self.velocity.is_finite() } /// Returns the distance from the origin, in kilometers. + /// + /// 返回到原点的距离,单位为千米。 pub fn radius(&self) -> f64 { self.position.norm() } /// Returns the speed, in kilometers per second. + /// + /// 返回速率,单位为千米每秒。 pub fn speed(&self) -> f64 { self.velocity.norm() } @@ -54,6 +73,14 @@ impl Sample { /// carry the endpoints, the second and fourth the slopes. The derivative /// terms are scaled by `dt` because the slopes are per unit time while `s` /// is per unit interval. +/// +/// 对一个分量求 Hermite 三次式的值。 +/// +/// `p0` 与 `p1` 为端点值,`v0` 与 `v1` 为端点处对时间的导数,`dt` 为区间长度, +/// `s` 为区间内位置比例,位于 `[0, 1]`。 +/// +/// 四项即标准三次 Hermite 基:第一、三项承载端点值,第二、四项承载斜率。导数项 +/// 乘以 `dt`,因为斜率以单位时间计而 `s` 以单位区间计。 pub fn hermite_position(p0: Vec3, v0: Vec3, p1: Vec3, v1: Vec3, dt: f64, s: f64) -> Vec3 { let s2 = s * s; let s3 = s2 * s; @@ -69,6 +96,11 @@ pub fn hermite_position(p0: Vec3, v0: Vec3, p1: Vec3, v1: Vec3, dt: f64, s: f64) /// This is the velocity that goes with [`hermite_position`]. It is the exact /// derivative of that cubic, so the reconstructed state is internally /// consistent: a caller that differentiates the position gets this velocity. +/// +/// 对一个分量求 Hermite 三次式的导数。 +/// +/// 这是与 [`hermite_position`] 配对的速度,且是该三次式的精确导数,因此重建 +/// 的状态内部自洽:调用方对位置求导会得到这个速度。 pub fn hermite_velocity(p0: Vec3, v0: Vec3, p1: Vec3, v1: Vec3, dt: f64, s: f64) -> Vec3 { let s2 = s * s; let dh00 = 6.0 * s2 - 6.0 * s; @@ -78,10 +110,14 @@ pub fn hermite_velocity(p0: Vec3, v0: Vec3, p1: Vec3, v1: Vec3, dt: f64, s: f64) // The chain rule turns the per interval derivative into a per second // one, which divides the position terms by `dt` and leaves the velocity // terms, already per second, untouched. + // 链式法则把每单位区间的导数变为每单位秒的导数:位置项除以 `dt`,而速度项 + // 本就以每秒计,保持不变。 p0 * (dh00 / dt) + v0 * dh10 + p1 * (dh01 / dt) + v1 * dh11 } /// Evaluates both the Hermite position and velocity. +/// +/// 同时求 Hermite 位置与速度。 pub fn hermite_state(p0: Vec3, v0: Vec3, p1: Vec3, v1: Vec3, dt: f64, s: f64) -> (Vec3, Vec3) { ( hermite_position(p0, v0, p1, v1, dt, s), @@ -97,6 +133,12 @@ pub fn hermite_state(p0: Vec3, v0: Vec3, p1: Vec3, v1: Vec3, dt: f64, s: f64) -> /// query outside the covered interval is refused rather than extrapolated, /// because the interpolation has no information there and an extrapolated /// orbit can diverge quickly. +/// +/// 以插值回答查询的采样状态表。 +/// +/// 该表至少含两个样本,时间严格递增。在样本时刻查询会精确返回该样本;两个样本 +/// 之间的查询返回穿过四个端点值的 Hermite 三次式。落在覆盖区间之外的查询会被 +/// 拒绝而非外推,因为插值在那里没有信息,而外推的轨道可能迅速发散。 #[derive(Debug, Clone, PartialEq)] pub struct Ephemeris { samples: Vec, @@ -107,6 +149,10 @@ impl Ephemeris { /// /// Returns an error when there are fewer than two samples, when a sample /// is not finite, or when the times do not strictly increase. + /// + /// 由时间递增的样本构建该表。 + /// + /// 当样本少于两个、某个样本非有限,或时间未严格递增时返回错误。 pub fn from_samples(samples: Vec) -> Result { if samples.len() < 2 { return Err(EphemerisError::new(EphemerisErrorKind::TooFewSamples) @@ -132,11 +178,15 @@ impl Ephemeris { } /// Returns the samples in time order. + /// + /// 返回按时间排列的样本。 pub fn samples(&self) -> &[Sample] { &self.samples } /// Returns the number of samples. + /// + /// 返回样本数。 pub fn len(&self) -> usize { self.samples.len() } @@ -146,21 +196,32 @@ impl Ephemeris { /// A table built through [`Ephemeris::from_samples`] is never empty, so /// this is always `false` for one that exists; it is provided so that /// generic code that accepts a slice can use the usual pair of methods. + /// + /// 当该表为空时返回 `true`。 + /// + /// 经 [`Ephemeris::from_samples`] 构建的表不会为空,故已存在的表恒返回 + /// `false`;提供它是为了让接受切片的泛型代码可以配上惯常的一对方法。 pub fn is_empty(&self) -> bool { self.samples.is_empty() } /// Returns the first sample time, in seconds. + /// + /// 返回首个样本时刻,单位为秒。 pub fn start_time(&self) -> f64 { self.samples.first().map_or(0.0, |sample| sample.seconds) } /// Returns the last sample time, in seconds. + /// + /// 返回末个样本时刻,单位为秒。 pub fn end_time(&self) -> f64 { self.samples.last().map_or(0.0, |sample| sample.seconds) } /// Returns the time the table covers, in seconds. + /// + /// 返回该表覆盖的时间,单位为秒。 pub fn duration(&self) -> f64 { self.end_time() - self.start_time() } @@ -168,6 +229,10 @@ impl Ephemeris { /// Returns `true` when a time falls within the covered interval. /// /// The interval is closed, so its endpoints are covered. + /// + /// 当某时刻落在覆盖区间内时返回 `true`。 + /// + /// 该区间是闭的,故其端点也被覆盖。 pub fn covers(&self, seconds: f64) -> bool { seconds.is_finite() && seconds >= self.start_time() && seconds <= self.end_time() } @@ -176,6 +241,10 @@ impl Ephemeris { /// /// Returns `None` when the spacing varies, which a caller checks before /// indexing samples by arithmetic. + /// + /// 当样本等间隔时返回标称步长。 + /// + /// 间隔不一致时返回 `None`;调用方应先检查这一点,再按算术方式索引样本。 pub fn uniform_step(&self) -> Option { let first = self.samples.windows(2).next()?[1].seconds - self.samples[0].seconds; let uniform = self @@ -186,6 +255,8 @@ impl Ephemeris { } /// Returns `true` when the samples are evenly spaced. + /// + /// 当样本等间隔时返回 `true`。 pub fn is_uniform(&self) -> bool { self.uniform_step().is_some() } @@ -194,6 +265,11 @@ impl Ephemeris { /// /// The index `i` is such that `samples[i].seconds <= seconds <= /// samples[i + 1].seconds`. A time outside the interval returns `None`. + /// + /// 返回包含某时刻的区间下标。 + /// + /// 下标 `i` 满足 `samples[i].seconds <= seconds <= samples[i + 1].seconds`。 + /// 落在该区间之外的时刻返回 `None`。 fn interval_of(&self, seconds: f64) -> Option { if !self.covers(seconds) { return None; @@ -201,6 +277,8 @@ impl Ephemeris { // A binary search for the last sample at or before the time. The // partition point is the first sample strictly after it, so the // interval starts one before, clamped to the last interval. + // 二分查找最后一个不晚于该时刻的样本。分区点是其后第一个严格靠后的样本, + // 故区间起点为其前一,并钳位到最后一个区间。 let after = self .samples .partition_point(|sample| sample.seconds <= seconds); @@ -212,6 +290,10 @@ impl Ephemeris { /// /// A time equal to a sample time returns that sample. Returns `None` /// when the time is outside the covered interval. + /// + /// 返回某时刻的状态,在样本之间插值。 + /// + /// 时刻与某样本相同时返回该样本。落在覆盖区间之外时返回 `None`。 pub fn state_at(&self, seconds: f64) -> Option { let index = self.interval_of(seconds)?; let first = self.samples[index]; @@ -219,6 +301,8 @@ impl Ephemeris { // At a sample the interpolation must reproduce the sample exactly, // which avoids the round off of evaluating the cubic at its end. + // 在样本处插值必须精确复现该样本,从而避免在三次式端点求值带来的 + // 舍入误差。 if seconds == first.seconds { return Some(first); } @@ -244,6 +328,10 @@ impl Ephemeris { /// /// This is for a caller that must produce a value for any time and is /// content to hold the last known state rather than fail. + /// + /// 返回某时刻的状态;时刻落在覆盖区间之外时返回最近的端点。 + /// + /// 适用于必须对任意时刻给出取值、且宁可保持最近已知状态而不愿失败的调用方。 pub fn clamped_state_at(&self, seconds: f64) -> Option { if self.covers(seconds) { return self.state_at(seconds); @@ -259,6 +347,8 @@ impl Ephemeris { } /// Returns the sample nearest to a time, without interpolating. + /// + /// 返回距某时刻最近的样本,不作插值。 pub fn nearest_sample(&self, seconds: f64) -> Option { if !seconds.is_finite() { return None; @@ -280,6 +370,8 @@ impl Ephemeris { } /// Returns the times of every sample, in order. + /// + /// 返回各样本时刻,按顺序排列。 pub fn times(&self) -> Vec { self.samples.iter().map(|sample| sample.seconds).collect() } @@ -288,6 +380,11 @@ impl Ephemeris { /// /// This is a convenience for a caller that wants to check a property of /// the whole table, such as every radius being above the surface, once. + /// + /// 当每个状态都满足给定谓词时返回 `true`。 + /// + /// 这是给需要一次性检查整表性质的调用方的便利方法,例如检查所有半径都在 + /// 表面之上。 pub fn all_samples(&self, predicate: F) -> bool where F: Fn(&Sample) -> bool, diff --git a/crates/orbit/apsis-ephemeris/src/error.rs b/crates/orbit/apsis-ephemeris/src/error.rs index 07441b0..25d80c8 100644 --- a/crates/orbit/apsis-ephemeris/src/error.rs +++ b/crates/orbit/apsis-ephemeris/src/error.rs @@ -3,20 +3,34 @@ use core::fmt; /// The category of an ephemeris error. +/// +/// 星历错误的类别。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum EphemerisErrorKind { /// The sampling grid has fewer than two samples. + /// + /// 采样网格少于两个样本。 TooFewSamples, /// A time or a step is not finite. + /// + /// 某时刻或步长非有限。 NotFinite, /// A step is zero or negative. + /// + /// 步长为零或为负。 NonPositiveStep, /// The samples are not in increasing time order. + /// + /// 样本未按时间递增排列。 NotMonotonic, /// A state produced by the sampler is not finite. + /// + /// 采样器产生的状态非有限。 InvalidState, /// A query time falls outside the covered interval. + /// + /// 查询时刻落在覆盖区间之外。 OutOfRange, } @@ -35,6 +49,8 @@ impl fmt::Display for EphemerisErrorKind { } /// A failure while building or querying an ephemeris. +/// +/// 构建或查询星历时发生的失败。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct EphemerisError { kind: EphemerisErrorKind, @@ -43,22 +59,30 @@ pub struct EphemerisError { impl EphemerisError { /// Creates an error with the given category. + /// + /// 用给定类别创建一个错误。 pub fn new(kind: EphemerisErrorKind) -> Self { Self { kind, detail: None } } /// Attaches a description of the value that caused the failure. + /// + /// 附加对该失败所涉数值的说明。 pub fn with_detail(mut self, detail: impl Into) -> Self { self.detail = Some(detail.into()); self } /// Returns the category of this failure. + /// + /// 返回该失败的类别。 pub const fn kind(&self) -> EphemerisErrorKind { self.kind } /// Returns the detail text attached to this error, if any. + /// + /// 返回附加在该错误上的说明文本,若有。 pub fn detail(&self) -> Option<&str> { self.detail.as_deref() } diff --git a/crates/orbit/apsis-ephemeris/src/sampler.rs b/crates/orbit/apsis-ephemeris/src/sampler.rs index b419532..f69d09f 100644 --- a/crates/orbit/apsis-ephemeris/src/sampler.rs +++ b/crates/orbit/apsis-ephemeris/src/sampler.rs @@ -12,18 +12,33 @@ use apsis_math::Vec3; /// the end, with the end itself included whether or not it falls on the /// grid. Including the end matters, because a caller that asks for coverage /// up to a time expects the table to reach it. +/// +/// 状态函数求值的均匀时间网格。 +/// +/// 采样器说明何时采样以及采样多密。网格由起始时刻、结束时刻与步长定义,样本数 +/// 随之而定:样本置于起点及此后每隔一步处,并始终包含结束时刻,无论它是否落在 +/// 网格上。包含结束时刻很重要,因为要求覆盖到某时刻的调用方期望表格确实延伸到 +/// 那里。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct Sampler { /// The time of the first sample, in seconds. + /// + /// 首个样本时刻,单位为秒。 pub start_seconds: f64, /// The time of the last sample, in seconds. + /// + /// 末个样本时刻,单位为秒。 pub end_seconds: f64, /// The spacing between samples, in seconds. + /// + /// 样本间隔,单位为秒。 pub step_seconds: f64, } impl Sampler { /// Creates a sampler over an interval with a step. + /// + /// 按给定步长在某区间上创建采样器。 pub const fn new(start_seconds: f64, end_seconds: f64, step_seconds: f64) -> Self { Self { start_seconds, @@ -38,6 +53,11 @@ impl Sampler { /// and the step must be finite and positive. A zero length interval is /// refused because a table needs at least two distinct times and the /// interpolation has no interval to work over. + /// + /// 当该采样器描述的网格可用时返回 `true`。 + /// + /// 各时刻须有限,结束时刻须严格晚于起始时刻,步长须有限且为正。零长度区间 + /// 会被拒绝,因为表格至少需要两个不同时刻,而插值没有区间可用。 pub fn is_valid(&self) -> bool { self.start_seconds.is_finite() && self.end_seconds.is_finite() @@ -51,6 +71,11 @@ impl Sampler { /// This is the number of whole steps that fit in the interval plus one /// for the start and one for the end when the end is off the grid. /// Returns `None` when the sampler is not usable. + /// + /// 返回该网格产生的样本数。 + /// + /// 它等于区间内容纳的整步数,加上起点那一个;当结束时刻不在网格上时再加 + /// 结束那一个。采样器不可用时返回 `None`。 pub fn sample_count(&self) -> Option { if !self.is_valid() { return None; @@ -59,6 +84,7 @@ impl Sampler { let whole_steps = (span / self.step_seconds).floor(); // The end sample is added unless it already coincides with the last // whole step within a rounding tolerance. + // 除非结束样本在舍入容差内已与最后一个整步重合,否则补上它。 let remainder = span - whole_steps * self.step_seconds; let extra = if remainder > self.step_seconds * 1e-9 { 1 @@ -73,6 +99,11 @@ impl Sampler { /// Returns `None` when the sampler is not usable. The last time is the /// end when the end does not fall on the grid, and the last grid point /// otherwise. + /// + /// 返回网格上的各时刻,按顺序排列。 + /// + /// 采样器不可用时返回 `None`。结束时刻不落在网格上时最后一项为结束时刻, + /// 否则为最后一个网格点。 pub fn times(&self) -> Option> { let count = self.sample_count()?; let mut times = Vec::with_capacity(count); @@ -84,6 +115,7 @@ impl Sampler { times.push(time); } // The end is always included, whether or not it landed on the grid. + // 无论结束时刻是否落在网格上,它总被包含。 if times.last().copied() != Some(self.end_seconds) { times.push(self.end_seconds); } @@ -99,6 +131,14 @@ impl Sampler { /// /// Returns an error when the sampler is not usable or when the function /// produces a state that is not finite. + /// + /// 在网格上求值状态函数以填充星历。 + /// + /// 该函数把以秒计的时刻映射为位置与速度。在本工作区它通常是 + /// `apsis_sgp4::Satellite::propagate_at` 上的闭包,但任何函数都可以,因此 + /// 采样器也可针对解析轨道测试。 + /// + /// 采样器不可用或函数产生非有限状态时返回错误。 pub fn sample(&self, mut state_at: F) -> Result where F: FnMut(f64) -> Sample, @@ -124,6 +164,8 @@ impl Sampler { // The time is set from the grid rather than trusted from the // function, so the table stays strictly ordered even if the // function returns a slightly different time. + // 时刻取自网格而不采信函数返回值,因此即便函数给出略有差异的时刻, + // 表格仍保持严格有序。 samples.push(Sample::new(time, sample.position, sample.velocity)); } Ephemeris::from_samples(samples) @@ -135,6 +177,11 @@ impl Sampler { /// central difference. This is for a position-only source, such as a /// tabulated file; where a velocity is available it should be used, so /// prefer [`Sampler::sample`]. + /// + /// 用只返回位置的函数填充星历。 + /// + /// 速度由相邻位置用中心差分重建。这适用于只有位置的数据源,例如制表文件; + /// 若可得速度应予使用,故优先选用 [`Sampler::sample`]。 pub fn sample_positions(&self, mut position_at: F) -> Result where F: FnMut(f64) -> Vec3, @@ -158,6 +205,8 @@ impl Sampler { // A central difference inside the interval and a one sided one at // each end. The finite difference is second order, which matches the // accuracy of the Hermite interpolation it feeds. + // 区间内部用中心差分,两端各用单边差分。该有限差分为二阶,与其所供给的 + // Hermite 插值精度相匹配。 let count = positions.len(); let mut samples = Vec::with_capacity(count); for (index, &position) in positions.iter().enumerate() { diff --git a/crates/orbit/apsis-kepler/src/anomaly.rs b/crates/orbit/apsis-kepler/src/anomaly.rs index ee8214d..04d37d7 100644 --- a/crates/orbit/apsis-kepler/src/anomaly.rs +++ b/crates/orbit/apsis-kepler/src/anomaly.rs @@ -24,16 +24,23 @@ use core::f64::consts::PI; /// The maximum number of Newton iterations used to solve Kepler's equation. +/// +/// 求解开普勒方程所用的 Newton 迭代最大步数。 const MAX_ITERATIONS: usize = 30; /// The convergence tolerance of the Newton iteration, in radians. +/// +/// Newton 迭代的收敛容差,单位为弧度。 const TOLERANCE: f64 = 1e-14; /// Reduces an angle into `[-pi, pi)`. +/// +/// 把角度归约到 `[-pi, pi)`。 pub fn wrap_to_pi(angle: f64) -> f64 { let wrapped = (angle + PI).rem_euclid(2.0 * PI) - PI; // `rem_euclid` can return exactly pi for an input just below pi, which // the half-open interval excludes. + // 对略小于 pi 的输入,`rem_euclid` 可能恰好返回 pi,而半开区间不含该端点。 if wrapped >= PI { wrapped - 2.0 * PI } else { @@ -52,6 +59,16 @@ pub fn wrap_to_pi(angle: f64) -> f64 { /// The half angle form is used rather than the direct cosine relation so /// that the result is correct in all four quadrants without a quadrant /// check. +/// +/// 由偏近点角与偏心率返回真近点角。 +/// +/// 该关系来自辅助圆的几何性质: +/// +/// ```text +/// tan(nu / 2) = sqrt((1 + e) / (1 - e)) tan(E / 2) +/// ``` +/// +/// 采用半角形式而非直接的余弦关系,使结果在四个象限中都正确,无需判别象限。 pub fn true_from_eccentric(eccentric_anomaly: f64, eccentricity: f64) -> f64 { let half = eccentric_anomaly / 2.0; let factor = ((1.0 + eccentricity) / (1.0 - eccentricity)).sqrt(); @@ -61,6 +78,10 @@ pub fn true_from_eccentric(eccentric_anomaly: f64, eccentricity: f64) -> f64 { /// Returns the eccentric anomaly for a true anomaly and eccentricity. /// /// This is the inverse of [`true_from_eccentric`]. +/// +/// 由真近点角与偏心率返回偏近点角。 +/// +/// 这是 [`true_from_eccentric`] 的逆运算。 pub fn eccentric_from_true(true_anomaly: f64, eccentricity: f64) -> f64 { let half = true_anomaly / 2.0; let factor = ((1.0 - eccentricity) / (1.0 + eccentricity)).sqrt(); @@ -79,9 +100,21 @@ pub fn eccentric_from_true(true_anomaly: f64, eccentricity: f64) -> f64 { /// proportional to the eccentricity, which keeps the iteration stable for /// eccentricities approaching one. The iteration always terminates, and the /// result is the value reached after at most a fixed number of steps. +/// +/// 由平近点角与偏心率返回偏近点角。 +/// +/// 用 Newton 法求解开普勒方程 +/// +/// ```text +/// M = E - e sin E +/// ``` +/// +/// 初值取平近点角加上一个正比于偏心率的项,使迭代在偏心率趋近一时保持稳定。 +/// 迭代总会终止,结果是至多固定步数后达到的值。 pub fn eccentric_from_mean(mean_anomaly: f64, eccentricity: f64) -> f64 { let mean = wrap_to_pi(mean_anomaly); // A first order correction to the circular guess. + // 对圆轨道猜测的一阶修正。 let mut eccentric = mean + eccentricity * mean.sin() * (1.0 + eccentricity * mean.cos()); for _ in 0..MAX_ITERATIONS { let residual = eccentric - eccentricity * eccentric.sin() - mean; @@ -101,6 +134,10 @@ pub fn eccentric_from_mean(mean_anomaly: f64, eccentricity: f64) -> f64 { /// Returns the mean anomaly for an eccentric anomaly and eccentricity. /// /// This is Kepler's equation evaluated directly, `M = E - e sin E`. +/// +/// 由偏近点角与偏心率返回平近点角。 +/// +/// 这是直接求值的开普勒方程,`M = E - e sin E`。 pub fn mean_from_eccentric(eccentric_anomaly: f64, eccentricity: f64) -> f64 { wrap_to_pi(eccentric_anomaly - eccentricity * eccentric_anomaly.sin()) } @@ -111,12 +148,19 @@ pub fn mean_from_eccentric(eccentric_anomaly: f64, eccentricity: f64) -> f64 { /// mean anomaly then follows from Kepler's equation. Going through the /// eccentric anomaly avoids the separate closed form for the swept area and /// keeps a single expression responsible for the relation. +/// +/// 由真近点角与偏心率返回平近点角。 +/// +/// 先把真近点角转为偏近点角,再由开普勒方程得平近点角。绕经偏近点角避免了 +/// 另立扫过面积的闭式解,使该关系只由一个表达式负责。 pub fn mean_from_true(true_anomaly: f64, eccentricity: f64) -> f64 { let eccentric_anomaly = eccentric_from_true(true_anomaly, eccentricity); mean_from_eccentric(eccentric_anomaly, eccentricity) } /// Returns the true anomaly for a mean anomaly and eccentricity. +/// +/// 由平近点角与偏心率返回真近点角。 pub fn true_from_mean(mean_anomaly: f64, eccentricity: f64) -> f64 { true_from_eccentric( eccentric_from_mean(mean_anomaly, eccentricity), diff --git a/crates/orbit/apsis-kepler/src/elements.rs b/crates/orbit/apsis-kepler/src/elements.rs index 3daa10e..2cf5de3 100644 --- a/crates/orbit/apsis-kepler/src/elements.rs +++ b/crates/orbit/apsis-kepler/src/elements.rs @@ -13,19 +13,38 @@ use apsis_math::Vec3; /// /// Angles are in radians and the semi-major axis is in kilometers. The orbit /// is elliptical, so the eccentricity must be below one. +/// +/// 轨道的六个开普勒根数。 +/// +/// 形状与大小由半长轴与偏心率给出,定向由倾角、升交点赤经与近地点幅角给出, +/// 轨道上的位置由真近点角给出。 +/// +/// 角度单位为弧度,半长轴单位为千米。轨道为椭圆,故偏心率必须小于一。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct KeplerianElements { /// Semi-major axis, in kilometers. Must be positive. + /// + /// 半长轴,单位为千米,必须为正。 pub semi_major_axis: f64, /// Eccentricity, dimensionless. Must be in `[0, 1)`. + /// + /// 偏心率,无量纲,必须位于 `[0, 1)`。 pub eccentricity: f64, /// Inclination, in radians. Must be in `[0, pi]`. + /// + /// 倾角,单位为弧度,必须位于 `[0, pi]`。 pub inclination: f64, /// Right ascension of the ascending node, in radians. + /// + /// 升交点赤经,单位为弧度。 pub right_ascension: f64, /// Argument of perigee, in radians. + /// + /// 近地点幅角,单位为弧度。 pub argument_of_perigee: f64, /// True anomaly, in radians. + /// + /// 真近点角,单位为弧度。 pub true_anomaly: f64, } @@ -35,16 +54,27 @@ pub struct KeplerianElements { /// Position is in kilometers and velocity is in kilometers per second. The /// frame has its first axis toward the reference direction, its third axis /// along the orbit normal, and its second completing a right-handed set. +/// +/// 卫星在根数所参照惯性系中的位置与速度。 +/// +/// 位置单位为千米,速度单位为千米每秒。该系第一轴指向参考方向,第三轴沿轨道法线, +/// 第二轴构成右手系。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct State { /// Position, in kilometers. + /// + /// 位置,单位为千米。 pub position: Vec3, /// Velocity, in kilometers per second. + /// + /// 速度,单位为千米每秒。 pub velocity: Vec3, } impl KeplerianElements { /// Returns `true` when the elements describe an elliptical orbit. + /// + /// 当这些根数描述一条椭圆轨道时返回 `true`。 pub fn is_valid(self) -> bool { self.semi_major_axis.is_finite() && self.semi_major_axis > 0.0 @@ -61,11 +91,17 @@ impl KeplerianElements { /// /// This is `a (1 - e^2)`, the distance from the focus to the orbit /// measured perpendicular to the major axis. + /// + /// 返回半通径,单位为千米。 + /// + /// 它等于 `a (1 - e^2)`,即从焦点沿垂直于长轴方向量到轨道的距离。 pub fn semi_latus_rectum(self) -> f64 { self.semi_major_axis * (1.0 - self.eccentricity * self.eccentricity) } /// Returns the semi-minor axis, in kilometers. + /// + /// 返回半短轴,单位为千米。 pub fn semi_minor_axis(self) -> f64 { self.semi_major_axis * (1.0 - self.eccentricity * self.eccentricity).sqrt() } @@ -73,6 +109,10 @@ impl KeplerianElements { /// Returns the perigee radius, in kilometers. /// /// This is the closest approach to the focus, `a (1 - e)`. + /// + /// 返回近地点半径,单位为千米。 + /// + /// 它是到焦点的最近距离 `a (1 - e)`。 pub fn perigee_radius(self) -> f64 { self.semi_major_axis * (1.0 - self.eccentricity) } @@ -80,6 +120,10 @@ impl KeplerianElements { /// Returns the apogee radius, in kilometers. /// /// This is the farthest point from the focus, `a (1 + e)`. + /// + /// 返回远地点半径,单位为千米。 + /// + /// 它是离焦点最远的点 `a (1 + e)`。 pub fn apogee_radius(self) -> f64 { self.semi_major_axis * (1.0 + self.eccentricity) } @@ -87,16 +131,24 @@ impl KeplerianElements { /// Returns the orbital period, in seconds. /// /// This is Kepler's third law, `T = 2 pi sqrt(a^3 / mu)`. + /// + /// 返回轨道周期,单位为秒。 + /// + /// 这是开普勒第三定律,`T = 2 pi sqrt(a^3 / mu)`。 pub fn period(self, parameter: GravityParameter) -> f64 { 2.0 * core::f64::consts::PI * (self.semi_major_axis.powi(3) / parameter.value()).sqrt() } /// Returns the eccentric anomaly corresponding to the true anomaly. + /// + /// 返回与真近点角对应的偏近点角。 pub fn eccentric_anomaly(self) -> f64 { crate::anomaly::eccentric_from_true(self.true_anomaly, self.eccentricity) } /// Returns the mean anomaly corresponding to the true anomaly. + /// + /// 返回与真近点角对应的平近点角。 pub fn mean_anomaly(self) -> f64 { crate::anomaly::mean_from_true(self.true_anomaly, self.eccentricity) } @@ -117,6 +169,21 @@ impl KeplerianElements { /// /// and then rotates by the argument of perigee, the inclination, and the /// right ascension of the ascending node. + /// + /// 把这些根数转换为位置与速度。 + /// + /// 当根数不是有效椭圆轨道时返回 `None`。 + /// + /// 换算先在近点坐标系中定位卫星,该系第一轴指向近地点、第三轴沿轨道法线, + /// + /// ```text + /// r = p / (1 + e cos nu) + /// x = r cos nu, y = r sin nu, z = 0 + /// vx = -sqrt(mu / p) sin nu + /// vy = sqrt(mu / p) (e + cos nu) + /// ``` + /// + /// 再依次按近地点幅角、倾角与升交点赤经旋转。 pub fn to_state(self, parameter: GravityParameter) -> Option { if !self.is_valid() { return None; @@ -135,6 +202,8 @@ impl KeplerianElements { // Rotate from the perifocal frame to the inertial frame by the // argument of perigee about the third axis, the inclination about // the first, and the right ascension about the third. + // 从近点坐标系旋转到惯性系:先绕第三轴转近地点幅角,再绕第一轴转倾角, + // 最后绕第三轴转赤经。 let rotation = perifocal_to_inertial( self.argument_of_perigee, self.inclination, @@ -152,6 +221,11 @@ impl KeplerianElements { /// or when the orbit is not elliptical. This conversion assumes the /// orbit is bound, so a parabolic or hyperbolic state is rejected rather /// than returned with an eccentricity at or above one. + /// + /// 由位置与速度返回对应的根数。 + /// + /// 当状态非有限、半径为零或轨道非椭圆时返回 `None`。该换算假定轨道受束缚, + /// 因此抛物线或双曲线状态被拒绝,而不是返回偏心率大于等于一的结果。 pub fn from_state(state: &State, parameter: GravityParameter) -> Option { let position = state.position; let velocity = state.velocity; @@ -169,15 +243,18 @@ impl KeplerianElements { // The specific angular momentum and the node vector, which together // give the orientation. + // 比角动量与交点向量,二者共同给出轨道定向。 let angular_momentum = position.cross(velocity); let angular_momentum_norm = angular_momentum.norm(); if angular_momentum_norm == 0.0 { // A purely radial trajectory has no defined orbit plane. + // 纯径向轨迹没有良定义的轨道平面。 return None; } // The eccentricity vector points toward perigee and has magnitude // equal to the eccentricity. + // 偏心率矢量指向近地点,其模等于偏心率。 let eccentricity_vector = position * (velocity_squared - mu / radius) - velocity * (radius * radial_velocity); let eccentricity = eccentricity_vector.norm() / mu; @@ -186,6 +263,7 @@ impl KeplerianElements { } // The specific mechanical energy gives the semi-major axis. + // 单位质量机械能给出半长轴。 let energy = velocity_squared / 2.0 - mu / radius; if energy == 0.0 { return None; @@ -205,6 +283,8 @@ impl KeplerianElements { // inclination and for a circular orbit respectively; in those cases // the standard convention sets them to zero and measures the angle // from the reference direction instead. + // 倾角为零时交点未定义,圆轨道时近地点幅角未定义;在这两种情形下,标准 + // 约定将其置零,并改为自参考方向量取该角。 let right_ascension = if node_norm == 0.0 { 0.0 } else { @@ -215,6 +295,7 @@ impl KeplerianElements { 0.0 } else if node_norm == 0.0 { // Equatorial orbit: measure the argument from the first axis. + // 赤道轨道:自第一轴量取该幅角。 wrap_to_pi(eccentricity_vector.y.atan2(eccentricity_vector.x)) } else { let value = (node.dot(eccentricity_vector) / (node_norm * eccentricity_vector.norm())) @@ -230,6 +311,8 @@ impl KeplerianElements { // The true anomaly follows from the eccentric anomaly recovered // from the radial distance and the radial velocity, which is more // accurate near a circular orbit than the angle between vectors. + // 真近点角由径向距离与径向速度求得的偏近点角推出;在近圆轨道处,这比 + // 直接求两矢量夹角更准确。 let true_anomaly = true_anomaly_from_state(position, velocity, eccentricity, mu); let elements = Self { @@ -244,6 +327,8 @@ impl KeplerianElements { } /// Returns a copy of these elements with a different true anomaly. + /// + /// 返回真近点角不同的这些根数的副本。 pub fn with_true_anomaly(self, true_anomaly: f64) -> Self { Self { true_anomaly, @@ -255,6 +340,10 @@ impl KeplerianElements { /// /// The true anomaly is recovered from the mean anomaly through Kepler's /// equation. + /// + /// 返回给定平近点角处这些根数的副本。 + /// + /// 真近点角由平近点角经开普勒方程求得。 pub fn with_mean_anomaly(self, mean_anomaly: f64) -> Self { let eccentric = eccentric_from_mean(mean_anomaly, self.eccentricity); Self { @@ -268,6 +357,11 @@ impl KeplerianElements { /// /// This is the same value as [`KeplerianElements::mean_anomaly`], named /// for symmetry with [`KeplerianElements::with_mean_anomaly`]. + /// + /// 返回平近点角,即沿轨道的时间线性度量。 + /// + /// 其值与 [`KeplerianElements::mean_anomaly`] 相同,此处命名是为了与 + /// [`KeplerianElements::with_mean_anomaly`] 对称。 pub fn to_mean_anomaly(self) -> f64 { mean_from_eccentric(self.eccentric_anomaly(), self.eccentricity) } @@ -278,6 +372,11 @@ impl KeplerianElements { /// The rotation is a product of three elementary rotations: by the argument /// of perigee about the third axis, then the inclination about the first, /// then the right ascension about the third. +/// +/// 返回从近点坐标系到惯性系的旋转。 +/// +/// 该旋转是三个基本旋转之积:先绕第三轴转近地点幅角,再绕第一轴转倾角,最后 +/// 绕第三轴转赤经。 pub fn perifocal_to_inertial( argument_of_perigee: f64, inclination: f64, @@ -290,6 +389,8 @@ pub fn perifocal_to_inertial( // Expanded product R3(O) R1(i) R3(w). Writing it out avoids three matrix // products and keeps the result well conditioned. + // 展开后的乘积 R3(O) R1(i) R3(w)。直接写出可省去三次矩阵相乘,并使结果 + // 保持良态。 Mat3::from_rows( [ cos_o * cos_w - sin_o * sin_w * cos_i, @@ -322,6 +423,20 @@ pub fn perifocal_to_inertial( /// handled by measuring the angle from the node instead, which is the same /// convention [`KeplerianElements::from_state`] uses for the argument of /// perigee. +/// +/// 由状态矢量反推真近点角。 +/// +/// 以偏近点角为中间量,可在近圆轨道处保持精度,因为此时偏心率矢量很短、方向 +/// 难以确定。偏近点角由径向距离与径向速度经下式求得: +/// +/// ```text +/// cos E = (1 - r / a) / e +/// sin E = r rdot / (e sqrt(mu a)) +/// ``` +/// +/// 真近点角再由偏近点角推出。对偏心率过小而使上述式子失效的圆轨道,改为 +/// 自交点量取夹角处理,这与 [`KeplerianElements::from_state`] 处理近地点幅角 +/// 时所用的约定相同。 fn true_anomaly_from_state(position: Vec3, velocity: Vec3, eccentricity: f64, mu: f64) -> f64 { let radius = position.norm(); let radial_velocity = position.dot(velocity) / radius; @@ -333,16 +448,20 @@ fn true_anomaly_from_state(position: Vec3, velocity: Vec3, eccentricity: f64, mu // Below this eccentricity the angle is recovered from the node instead, // because the division by the eccentricity would amplify the rounding // in the radial terms. + // 偏心率低于此值时改为自交点反求夹角,因为除以偏心率会放大径向项中的 + // 舍入误差。 const CIRCULAR_THRESHOLD: f64 = 1e-8; if eccentricity < CIRCULAR_THRESHOLD { let angular_momentum = position.cross(velocity); let node = Vec3::Z.cross(angular_momentum); if node.norm() == 0.0 { // Equatorial circular orbit: measure from the first axis. + // 赤道圆轨道:自第一轴量取。 return wrap_to_pi(position.y.atan2(position.x)); } // The argument of latitude is the angle from the node to the // position, measured in the orbit plane. + // 纬度幅角是自交点到该位置在轨道平面内量取的夹角。 let node_unit = node.normalize().expect("node is non-zero"); let in_plane = position.normalize().expect("radius is non-zero"); let cross = node_unit.cross(in_plane); diff --git a/crates/orbit/apsis-kepler/src/parameter.rs b/crates/orbit/apsis-kepler/src/parameter.rs index 2d2403a..2474824 100644 --- a/crates/orbit/apsis-kepler/src/parameter.rs +++ b/crates/orbit/apsis-kepler/src/parameter.rs @@ -7,23 +7,36 @@ /// models, and the correct choice is the one that matches the source of the /// orbital elements. For elements derived from a two-line element set, the /// WGS 72 value is the one the SGP4 theory was fitted to. +/// +/// 中心天体的引力参数,单位为立方千米每二次方秒。 +/// +/// 提供两个值是因为工作区支持两种重力模型,正确选择取决于轨道根数的来源。对 +/// 由两线根数集导出的根数,应取 WGS 72 值,即 SGP4 理论拟合所用的那一个。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct GravityParameter(pub f64); impl GravityParameter { /// The WGS 72 gravitational parameter, `398 600.8 km^3/s^2`. + /// + /// WGS 72 引力参数,`398 600.8 km^3/s^2`。 pub const WGS72: Self = Self(398_600.8); /// The WGS 84 gravitational parameter, `398 600.5 km^3/s^2`. + /// + /// WGS 84 引力参数,`398 600.5 km^3/s^2`。 pub const WGS84: Self = Self(398_600.5); /// Returns the value in cubic kilometers per second squared. + /// + /// 返回该值,单位为立方千米每二次方秒。 pub const fn value(self) -> f64 { self.0 } /// Returns the square root of the parameter, in kilometers to the three /// halves per second. + /// + /// 返回该参数的平方根,单位为千米的三分之二次方每秒。 pub fn sqrt(self) -> f64 { self.0.sqrt() } diff --git a/crates/orbit/apsis-maneuver/src/burn.rs b/crates/orbit/apsis-maneuver/src/burn.rs index 43eacae..de32d8d 100644 --- a/crates/orbit/apsis-maneuver/src/burn.rs +++ b/crates/orbit/apsis-maneuver/src/burn.rs @@ -16,11 +16,24 @@ use apsis_math::Vec3; /// fires along a body axis that rotates as the spacecraft turns to hold a /// direction, and modelling that requires the attitude history, which is not /// part of this crate. +/// +/// 在一段时间内施加恒定加速度的机动。 +/// +/// 有限推力点火适用于推力器工作时间足够长、其时长无法忽略的情形:电推力器的 +/// 点火可能持续轨道相当大一部分,任何推力期间航天器有明显位移的点火亦属此类。 +/// +/// 加速度在调用方选定的惯性系中保持恒定。这是本模型所作的简化:真实推力器 +/// 沿随航天器转动以保持方向的本体轴喷焰,建模这一点需要姿态历史,而它不属 +/// 于本 crate 的范围。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct FiniteBurn { /// The acceleration vector, in kilometers per second squared. + /// + /// 加速度矢量,单位为千米每二次方秒。 acceleration: Vec3, /// The duration, in seconds. + /// + /// 时长,单位为秒。 duration: f64, } @@ -29,6 +42,10 @@ impl FiniteBurn { /// /// Returns an error when a value is not finite, when the duration is not /// positive, or when the acceleration is zero. + /// + /// 由加速度与时长创建一次点火。 + /// + /// 当某个值非有限、时长不为正或加速度为零时返回错误。 pub fn new(acceleration: Vec3, duration: f64) -> Result { if !acceleration.is_finite() || !duration.is_finite() { return Err(ManeuverError::new(ManeuverErrorKind::NotFinite)); @@ -53,6 +70,10 @@ impl FiniteBurn { /// /// The direction is normalized, so the acceleration magnitude is fixed /// whatever the length of the direction vector. + /// + /// 沿给定方向创建指定大小加速度的点火。 + /// + /// 方向被归一化,故无论方向矢量多长,加速度大小都固定不变。 pub fn along(direction: Vec3, acceleration: f64, duration: f64) -> Result { if !acceleration.is_finite() || acceleration <= 0.0 { return Err( @@ -68,17 +89,23 @@ impl FiniteBurn { } /// Returns the acceleration vector, in kilometers per second squared. + /// + /// 返回加速度矢量,单位为千米每二次方秒。 pub const fn acceleration(self) -> Vec3 { self.acceleration } /// Returns the magnitude of the acceleration, in kilometers per second /// squared. + /// + /// 返回加速度大小,单位为千米每二次方秒。 pub fn acceleration_magnitude(self) -> f64 { self.acceleration.norm() } /// Returns the duration, in seconds. + /// + /// 返回时长,单位为秒。 pub const fn duration(self) -> f64 { self.duration } @@ -91,6 +118,12 @@ impl FiniteBurn { /// achieved change is close to this for a short burn and differs from it /// for a long one, which is the difference the finite model exists to /// capture. + /// + /// 返回该加速度在自由空间中会造成的速度变化,单位为千米每秒。 + /// + /// 它是加速度乘以时长,即点火在不受引力作用时会提供的 delta-v。在引力 + /// 作用下,短点火的实际变化与之接近,长点火则不同,这正是有限推力模型 + /// 得以存在的意义。 pub fn nominal_delta_v(self) -> f64 { self.acceleration.norm() * self.duration } @@ -100,6 +133,11 @@ impl FiniteBurn { /// This is the impulsive burn the finite one approximates as its /// duration shrinks, and it is what [`FiniteBurn::apply`] returns in the /// limit of a constant gravity field. + /// + /// 返回与标称变化等效的冲量。 + /// + /// 它是有限点火在时长趋零时所逼近的脉冲点火,也是 [`FiniteBurn::apply`] + /// 在恒定引力场极限下给出的量。 pub fn equivalent_impulse(self) -> Result { crate::impulse::Impulse::new(self.acceleration * self.duration) } @@ -124,6 +162,20 @@ impl FiniteBurn { /// /// Returns the state at the end of the burn together with the achieved /// velocity change. + /// + /// 在二体引力下把该点火作用于一个状态。 + /// + /// 位置与速度在时长内积分前进,积分时把恒定加速度叠加到引力上。恒定加速度 + /// 加质点引力的精确解没有闭式,故用定步长四阶 Runge-Kutta 积分,对点火所占 + /// 的短弧段是精确的。 + /// + /// 实际达到的速度变化以同时长的滑行作为比较基准,而非以初始速度。区分这 + /// 一点很重要:即使无推力,引力在点火期间也会改变速度,故末速度与初速度 + /// 之差含速度矢量的自然转动。与滑行比较才能隔离出推力的贡献,即该次点火 + /// 实际买到的东西;这也使结果在时长趋零时收敛到标称变化,而与初速度 + /// 作差则不然。 + /// + /// 返回点火结束时的状态及实际达到的速度变化。 pub fn apply( self, state: State, @@ -141,6 +193,7 @@ impl FiniteBurn { let step = self.duration / steps as f64; // The two-body acceleration at a position. + // 某位置处的二体加速度。 let gravity = |position: Vec3| -> Vec3 { let radius = position.norm(); if radius == 0.0 { @@ -149,9 +202,11 @@ impl FiniteBurn { position * (-mu / (radius * radius * radius)) }; // The thrust added to gravity, which is what the burn integrates. + // 叠加到引力上的推力,即点火所积分的量。 let thrust_and_gravity = |position: Vec3| -> Vec3 { gravity(position) + self.acceleration }; // The burn trajectory. + // 点火轨迹。 let mut position = state.position; let mut velocity = state.velocity; for _ in 0..steps { @@ -163,6 +218,7 @@ impl FiniteBurn { // The coast trajectory over the same duration, with the same // integrator and step, so the only difference is the thrust. + // 同时长、同积分器同步长的滑行轨迹,故二者唯一差别是推力。 let mut coast_position = state.position; let mut coast_velocity = state.velocity; for _ in 0..steps { @@ -192,6 +248,11 @@ impl FiniteBurn { /// not valid, when the integration fails, or when the resulting state is /// no longer an elliptical orbit, which happens when the burn is large /// enough to escape. + /// + /// 把该点火作用于一组开普勒根数。 + /// + /// 先把根数转为状态,对状态施加点火,再转回根数。当根数无效、积分失败,或 + /// 结果状态不再是椭圆轨道时返回错误;后者在点火足够大以致逃逸时发生。 pub fn apply_to_elements( self, elements: KeplerianElements, @@ -211,21 +272,36 @@ impl FiniteBurn { /// The outcome keeps the final state, the change the burn achieved, and the /// change the acceleration would have given in free space, so a caller can /// see how much gravity cost or added during the burn. +/// +/// 一次有限推力点火的产物。 +/// +/// 结果保留末状态、点火实际达到的变化,以及该加速度在自由空间中会造成的 +/// 变化,使调用方可以看出点火期间引力造成或增加了多少。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct BurnOutcome { /// The state at the end of the burn. + /// + /// 点火结束时的状态。 pub state: State, /// The velocity change actually achieved, in kilometers per second. + /// + /// 实际达到的速度变化,单位为千米每秒。 pub achieved_delta_v: Vec3, /// The velocity change the acceleration would give in free space, in /// kilometers per second. + /// + /// 该加速度在自由空间中会造成的速度变化,单位为千米每秒。 pub nominal_delta_v: f64, /// The burn duration, in seconds. + /// + /// 点火时长,单位为秒。 pub duration: f64, } impl BurnOutcome { /// Returns the magnitude of the achieved velocity change. + /// + /// 返回实际达到的速度变化的大小。 pub fn achieved_magnitude(&self) -> f64 { self.achieved_delta_v.norm() } @@ -235,6 +311,11 @@ impl BurnOutcome { /// /// A negative value means gravity turned the velocity against the thrust /// during the burn, so the burn bought less than its free space delta-v. + /// + /// 返回实际变化与标称变化之差,单位为千米每秒。 + /// + /// 负值表示点火期间引力把速度转向了推力的反方向,使该次点火的收获小于其 + /// 自由空间 delta-v。 pub fn gravity_loss(&self) -> f64 { self.achieved_magnitude() - self.nominal_delta_v } @@ -243,6 +324,10 @@ impl BurnOutcome { /// /// Returns `None` when the final state is not an elliptical orbit, which /// happens when the burn is large enough to escape. + /// + /// 返回点火结束时的根数。 + /// + /// 当末状态不是椭圆轨道时返回 `None`;这在点火足够大以致逃逸时发生。 pub fn elements(&self, parameter: GravityParameter) -> Option { KeplerianElements::from_state(&self.state, parameter) } @@ -255,6 +340,11 @@ impl BurnOutcome { /// `dr/dt = v` and `dv/dt = a(r)`, with `a` combining thrust and gravity. /// The four stages are the standard ones; writing them out keeps the loop /// allocation free. +/// +/// 二体方程加上恒定加速度的一步经典四阶 Runge-Kutta。 +/// +/// 状态为 `(位置, 速度)`,方程即通常的 `dr/dt = v` 与 `dv/dt = a(r)`,其中 +/// `a` 合并推力与引力。四个级次是标准形式;直接写出使循环无需分配内存。 fn rk4_step( position: Vec3, velocity: Vec3, diff --git a/crates/orbit/apsis-maneuver/src/error.rs b/crates/orbit/apsis-maneuver/src/error.rs index f83ccbd..0ae7772 100644 --- a/crates/orbit/apsis-maneuver/src/error.rs +++ b/crates/orbit/apsis-maneuver/src/error.rs @@ -3,20 +3,34 @@ use core::fmt; /// The category of a maneuver error. +/// +/// 机动错误的类别。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum ManeuverErrorKind { /// A velocity change, acceleration, or duration is not finite. + /// + /// 速度变化、加速度或时长非有限。 NotFinite, /// The duration of a burn is not positive. + /// + /// 点火时长不为正。 NonPositiveDuration, /// The acceleration of a burn is not positive. + /// + /// 点火加速度不为正。 NonPositiveAcceleration, /// A radius is not finite or not positive. + /// + /// 某半径非有限或不为正。 InvalidRadius, /// The two radii of a transfer are equal, so there is nothing to do. + /// + /// 转移的两个半径相等,因而无事可做。 DegenerateTransfer, /// A state is missing the data the operation needs. + /// + /// 某状态缺少该运算所需的数据。 InvalidState, } @@ -35,6 +49,8 @@ impl fmt::Display for ManeuverErrorKind { } /// A failure while building or applying a maneuver. +/// +/// 构建或应用机动时发生的失败。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ManeuverError { kind: ManeuverErrorKind, @@ -43,22 +59,30 @@ pub struct ManeuverError { impl ManeuverError { /// Creates an error with the given category. + /// + /// 用给定类别创建一个错误。 pub fn new(kind: ManeuverErrorKind) -> Self { Self { kind, detail: None } } /// Attaches a description of the value that caused the failure. + /// + /// 附加对该失败所涉数值的说明。 pub fn with_detail(mut self, detail: impl Into) -> Self { self.detail = Some(detail.into()); self } /// Returns the category of this failure. + /// + /// 返回该失败的类别。 pub const fn kind(&self) -> ManeuverErrorKind { self.kind } /// Returns the detail text attached to this error, if any. + /// + /// 返回附加在该错误上的说明文本,若有。 pub fn detail(&self) -> Option<&str> { self.detail.as_deref() } diff --git a/crates/orbit/apsis-maneuver/src/hohmann.rs b/crates/orbit/apsis-maneuver/src/hohmann.rs index 77efeb9..f21fdb7 100644 --- a/crates/orbit/apsis-maneuver/src/hohmann.rs +++ b/crates/orbit/apsis-maneuver/src/hohmann.rs @@ -16,21 +16,43 @@ use apsis_kepler::GravityParameter; /// raises the orbit and a negative one lowers it, so the same type covers a /// climb and a descent. The total is the sum of the magnitudes, which is the /// quantity a propellant budget uses. +/// +/// 两个共面圆轨道之间的双脉冲转移。 +/// +/// 该转移即经典的 Hohmann 机动:一次点火进入椭圆,其近地点相切于较低轨道、 +/// 远地点相切于较高轨道,沿该椭圆滑行半个周期,再一次点火进入目标圆轨道。它 +/// 是脉冲模型中两个共面圆轨道之间最省的双脉冲转移,因此成为衡量其他转移的 +/// 基准。 +/// +/// 两次点火沿运动方向带符号:正值抬升轨道,负值降低轨道,故同一类型同时 +/// 表达上升与下降。总和取两者模之和,即推进剂预算所用的量。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct HohmannTransfer { /// The radius of the orbit the transfer starts from, in kilometers. + /// + /// 转移起始轨道的半径,单位为千米。 pub from_radius: f64, /// The radius of the orbit the transfer arrives at, in kilometers. + /// + /// 转移到达轨道的半径,单位为千米。 pub to_radius: f64, /// The semi-major axis of the transfer ellipse, in kilometers. + /// + /// 转移椭圆的半长轴,单位为千米。 pub transfer_semi_major_axis: f64, /// The first burn, along the direction of motion, in kilometers per /// second. + /// + /// 第一次点火,沿运动方向,单位为千米每秒。 pub first_burn: f64, /// The second burn, along the direction of motion, in kilometers per /// second. + /// + /// 第二次点火,沿运动方向,单位为千米每秒。 pub second_burn: f64, /// The coast time between the burns, in seconds. + /// + /// 两次点火之间的滑行时间,单位为秒。 pub transfer_time: f64, } @@ -39,21 +61,31 @@ impl HohmannTransfer { /// /// This is the sum of the magnitudes of the two burns, which is what the /// transfer costs whatever its direction. + /// + /// 返回总速度变化,单位为千米每秒。 + /// + /// 它是两次点火大小之和,即无论方向如何该转移所需的代价。 pub fn total_delta_v(&self) -> f64 { self.first_burn.abs() + self.second_burn.abs() } /// Returns `true` when the transfer raises the orbit. + /// + /// 当该转移抬升轨道时返回 `true`。 pub fn is_raising(&self) -> bool { self.to_radius > self.from_radius } /// Returns the velocity change of the first burn, unsigned. + /// + /// 返回第一次点火的速度变化,不带符号。 pub fn first_burn_magnitude(&self) -> f64 { self.first_burn.abs() } /// Returns the velocity change of the second burn, unsigned. + /// + /// 返回第二次点火的速度变化,不带符号。 pub fn second_burn_magnitude(&self) -> f64 { self.second_burn.abs() } @@ -63,6 +95,10 @@ impl HohmannTransfer { /// /// This is the semi-major axis of the ellipse, which is the arithmetic /// mean of the two orbit radii. + /// + /// 返回该转移所对称的平均半径,单位为千米。 + /// + /// 它即该椭圆的半长轴,为两个轨道半径的算术平均。 pub fn mean_radius(&self) -> f64 { self.transfer_semi_major_axis } @@ -86,6 +122,20 @@ impl HohmannTransfer { /// the ellipse there, taken the other way round because the ellipse arrives /// slower than the circle it must join. The coast time is half the period of /// the ellipse. +/// +/// 返回两个圆轨道之间的双脉冲转移。 +/// +/// 半径单位为千米,须有限且为正。二者可按任意次序给出:第二个半径较大者为 +/// 上升,较小者为下降。 +/// +/// 当某半径不可用或二者相等时返回错误,因为轨道到其自身的转移不是机动,且 +/// 几乎总是计划中的笔误。 +/// +/// 两次点火由活力公式导出。在第一个半径处圆轨道速度为 `sqrt(mu / r1)`,而 +/// 转移椭圆上的速度为 `sqrt(mu (2 / r1 - 1 / a))`,其中 `a` 为椭圆半长轴, +/// 第一次点火即二者之差。第二次点火是目标处圆轨道速度与椭圆上速度之差,取 +/// 相反方向,因为椭圆到达时比它必须汇入的圆轨道更慢。滑行时间为该椭圆 +/// 周期的一半。 pub fn hohmann_transfer( from_radius: f64, to_radius: f64, @@ -106,11 +156,13 @@ pub fn hohmann_transfer( let transfer_semi_major_axis = (from_radius + to_radius) / 2.0; // The circular speeds at each radius. + // 各半径处的圆轨道速度。 let circular_from = (mu / from_radius).sqrt(); let circular_to = (mu / to_radius).sqrt(); // The speeds on the transfer ellipse at each radius, from the vis-viva // equation `v^2 = mu (2 / r - 1 / a)`. + // 各半径处转移椭圆上的速度,由活力公式 `v^2 = mu (2 / r - 1 / a)` 得出。 let transfer_from = (mu * (2.0 / from_radius - 1.0 / transfer_semi_major_axis)).sqrt(); let transfer_to = (mu * (2.0 / to_radius - 1.0 / transfer_semi_major_axis)).sqrt(); @@ -118,10 +170,13 @@ pub fn hohmann_transfer( // from the ellipse speed up to the circular speed at the destination. // Both are along the direction of motion, and the sign follows from // which speed is larger. + // 第一次点火把卫星加速到椭圆上;第二次把椭圆上的速度加速到目标处圆轨道 + // 速度。两者均沿运动方向,符号取决于哪个速度更大。 let first_burn = transfer_from - circular_from; let second_burn = circular_to - transfer_to; // Half the period of the transfer ellipse. + // 转移椭圆周期的一半。 let transfer_time = core::f64::consts::PI * (transfer_semi_major_axis.powi(3) / mu).sqrt(); Ok(HohmannTransfer { diff --git a/crates/orbit/apsis-maneuver/src/impulse.rs b/crates/orbit/apsis-maneuver/src/impulse.rs index 035caf9..7855078 100644 --- a/crates/orbit/apsis-maneuver/src/impulse.rs +++ b/crates/orbit/apsis-maneuver/src/impulse.rs @@ -14,6 +14,14 @@ use apsis_math::Vec3; /// be applied to, so the caller is responsible for expressing it in that /// frame. The magnitude is the delta-v, the quantity a propellant budget is /// measured in. +/// +/// 瞬时速度变化。 +/// +/// 冲量是在零时间内施加的速度变化,故只改变状态的速度而不改变其位置。这是相对 +/// 轨道周期很短的点火的模型,也是编写 delta-v 预算所用的模型。 +/// +/// 该变化以矢量形式存储在与待施加状态相同的坐标系中,因此调用方负责把它表达 +/// 在该系中。其模即 delta-v,推进剂预算以此计量。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct Impulse { delta_v: Vec3, @@ -26,6 +34,11 @@ impl Impulse { /// Returns an error when a component is not finite or when the vector is /// zero, because a zero change is not a maneuver and is almost always a /// mistake in a plan. + /// + /// 由速度变化矢量创建冲量,单位为千米每秒。 + /// + /// 当某个分量非有限或矢量为零时返回错误,因为零变化不是机动,且几乎总是 + /// 计划中的笔误。 pub fn new(delta_v: Vec3) -> Result { if !delta_v.is_finite() { return Err(ManeuverError::new(ManeuverErrorKind::NotFinite) @@ -44,6 +57,11 @@ impl Impulse { /// the length of the direction vector. Returns an error when the /// magnitude is not positive and finite or when the direction is zero or /// not finite. + /// + /// 沿给定方向创建指定大小的冲量。 + /// + /// 方向被归一化,故无论方向矢量多长,其大小即 delta-v。当大小非正或非有限, + /// 或方向为零或非有限时返回错误。 pub fn along(direction: Vec3, magnitude: f64) -> Result { if !magnitude.is_finite() || magnitude <= 0.0 { return Err( @@ -59,6 +77,8 @@ impl Impulse { } /// Returns the velocity change vector, in kilometers per second. + /// + /// 返回速度变化矢量,单位为千米每秒。 pub const fn delta_v(self) -> Vec3 { self.delta_v } @@ -66,6 +86,10 @@ impl Impulse { /// Returns the magnitude of the change, in kilometers per second. /// /// This is the delta-v the burn costs. + /// + /// 返回该变化的大小,单位为千米每秒。 + /// + /// 这就是该次点火所需的 delta-v。 pub fn magnitude(self) -> f64 { self.delta_v.norm() } @@ -75,11 +99,18 @@ impl Impulse { /// A factor of zero produces a zero impulse, which [`Impulse::new`] /// rejects, so this returns an error for it rather than a value that /// cannot be built. + /// + /// 返回该变化按某因子缩放后的副本。 + /// + /// 因子为零会得到零冲量,而 [`Impulse::new`] 拒绝这种情况,故本函数对该 + /// 因子返回错误,而不是给出一个无法构造的值。 pub fn scaled(self, factor: f64) -> Result { Self::new(self.delta_v * factor) } /// Returns a copy of the change reversed, which undoes the burn. + /// + /// 返回取反后的变化副本,即撤销该次点火。 pub fn reversed(self) -> Self { Self { delta_v: -self.delta_v, @@ -87,6 +118,8 @@ impl Impulse { } /// Applies the impulse to a velocity, returning the new velocity. + /// + /// 把该冲量作用于一个速度,返回新的速度。 pub fn apply_to_velocity(self, velocity: Vec3) -> Vec3 { velocity + self.delta_v } diff --git a/crates/orbit/apsis-orbit/src/orbit.rs b/crates/orbit/apsis-orbit/src/orbit.rs index 083cd93..51f37a8 100644 --- a/crates/orbit/apsis-orbit/src/orbit.rs +++ b/crates/orbit/apsis-orbit/src/orbit.rs @@ -13,6 +13,11 @@ use core::fmt; /// The type pairs a prepared SGP4 satellite with the gravity model it was /// initialised against, because the osculating element conversion needs the /// same gravitational parameter to be consistent with the propagation. +/// +/// 以平均根数形式持有、随时可传播的卫星轨道。 +/// +/// 该类型把准备好的 SGP4 卫星与其初始化时所用的重力模型配对,因为密切根数 +/// 换算需要同一个引力参数才能与传播保持一致。 #[derive(Debug, Clone)] pub struct Orbit { satellite: Satellite, @@ -26,6 +31,11 @@ impl Orbit { /// against, which for a catalogued satellite means /// [`GravityModel::WGS72`]. Returns `None` when the elements are not /// propagatable. + /// + /// 由已解析的根数集与重力模型准备轨道。 + /// + /// 重力模型须与该根数集拟合时所用者一致;对编目卫星即 [`GravityModel::WGS72`]。 + /// 当根数不可传播时返回 `None`。 pub fn from_element(element: &TwoLineElement, model: GravityModel) -> Option { Self::try_from_element(element, model).ok() } @@ -33,6 +43,10 @@ impl Orbit { /// Prepares an orbit from a parsed element set and a gravity model. /// /// Returns an error describing which element is not propagatable. + /// + /// 由已解析的根数集与重力模型准备轨道。 + /// + /// 返回一个说明哪个根数不可传播的错误。 pub fn try_from_element( element: &TwoLineElement, model: GravityModel, @@ -45,6 +59,8 @@ impl Orbit { } /// Prepares an orbit from mean elements already in model units. + /// + /// 由已按模型单位表示的平均根数准备轨道。 pub fn from_elements( elements: &Elements, model: GravityModel, @@ -57,6 +73,8 @@ impl Orbit { } /// Returns the underlying propagator. + /// + /// 返回底层的传播器。 pub fn satellite(&self) -> &Satellite { &self.satellite } @@ -65,6 +83,10 @@ impl Orbit { /// /// The value is the one used for the osculating element conversion, so /// that the conversion is consistent with the propagation. + /// + /// 返回与该重力模型配对的引力参数。 + /// + /// 该值即密切根数换算所用者,从而使换算与传播保持一致。 pub fn gravity_parameter(&self) -> GravityParameter { self.parameter } @@ -74,11 +96,17 @@ impl Orbit { /// /// The epoch is the value written in the element set, which is the /// origin the propagator measures time from. + /// + /// 返回根数历元,为 UTC 标度上的修正儒略日(Modified Julian Date)。 + /// + /// 该历元即根数集中写下的值,也是传播器度量的时间原点。 pub fn epoch_mjd(&self) -> f64 { self.satellite.epoch_mjd() } /// Returns `true` when the propagator uses the deep space branch. + /// + /// 当传播器使用深空分支时返回 `true`。 pub fn is_deep_space(&self) -> bool { self.satellite.is_deep_space() } @@ -87,11 +115,17 @@ impl Orbit { /// /// Position is in kilometers and velocity in kilometers per second, in /// the True Equator Mean Equinox frame. + /// + /// 返回距根数历元若干分钟处的状态。 + /// + /// 位置单位为千米,速度单位为千米每秒,位于真赤道平春分点系。 pub fn propagate_minutes(&self, minutes: f64) -> Result { self.satellite.propagate_minutes(minutes) } /// Returns the state at a Modified Julian Date on the UTC scale. + /// + /// 返回 UTC 标度上某修正儒略日(Modified Julian Date)处的状态。 pub fn propagate_at(&self, utc_mjd: f64) -> Result { self.propagate_minutes((utc_mjd - self.satellite.epoch_mjd()) * 1_440.0) } @@ -107,10 +141,19 @@ impl Orbit { /// Returns an error when the state cannot be propagated, or when the /// instantaneous orbit is not elliptical, which can happen for a state /// the propagation has pushed onto an escape trajectory. + /// + /// 返回距根数历元若干分钟处的密切开普勒根数。 + /// + /// 密切根数描述与该瞬时位置和速度相匹配的二体轨道。它不是根数集的平均根数, + /// 且沿轨道变化,因为 SGP4 含有二体模型所没有的摄动。 + /// + /// 当状态无法传播,或瞬时轨道非椭圆时返回错误;后者可发生于传播把状态推上 + /// 逃逸轨迹的情形。 pub fn osculating_elements(&self, minutes: f64) -> Result { let state = self.propagate_minutes(minutes)?; // The two crates define their own state type, so the components are // transferred explicitly rather than relying on a shared type. + // 两个 crate 各自定义状态类型,故显式转移各分量,而不依赖共享类型。 let kepler_state = apsis_kepler::State { position: state.position, velocity: state.velocity, @@ -125,12 +168,18 @@ impl Orbit { /// /// This is a convenience for the common case of wanting only the /// distance from the center of the Earth. + /// + /// 返回状态矢量的模。 + /// + /// 这为只需到地心距离的常见情形提供便利。 pub fn radius_at(&self, minutes: f64) -> Result { self.propagate_minutes(minutes) .map(|state| state.position.norm()) } /// Returns the position at a number of minutes from the element epoch. + /// + /// 返回距根数历元若干分钟处的位置。 pub fn position_at(&self, minutes: f64) -> Result { self.propagate_minutes(minutes).map(|state| state.position) } @@ -150,6 +199,18 @@ impl Orbit { /// Returns an error when the grid is not usable or when a sample cannot /// be propagated, which happens when the elements have decayed or the /// range leaves the region where the model is defined. + /// + /// 把该轨道在若干分钟的区间上采样为星历。 + /// + /// 传播是生成轨迹中开销最大的部分,故一次建表、多次查询值得占用内存。采样 + /// 时刻以距根数历元的分钟计,与本类型其余部分一致;返回的星历则用秒,与 + /// `apsis-ephemeris` 一致。 + /// + /// 该表用传播器给出的位置与速度建立,故 Hermite 插值具有精确的端点斜率, + /// 样本间误差极小;除数据源没有速度外,无需使用仅位置的路径。 + /// + /// 当网格不可用或某样本无法传播时返回错误;后者发生于根数已衰减,或区间 + /// 超出模型定义范围的情形。 pub fn sample_ephemeris( &self, start_minutes: f64, @@ -168,12 +229,15 @@ impl Orbit { let ephemeris = sampler.sample(|seconds| { // The minutes are recovered from the seconds the sampler uses, // so a non-integer step stays consistent with the grid. + // 由采样器所用的秒数反推分钟,使非整数步长仍与网格一致。 match self.propagate_minutes(seconds / 60.0) { Ok(state) => apsis_ephemeris::Sample::new(seconds, state.position, state.velocity), Err(error) => { // The sampler cannot report the propagation failure, so // it is recorded and the sample is filled with a value // the table constructor will then reject. + // 采样器无法报告传播失败,故在此记录,并用表格构造器随后 + // 会拒绝的值填充该样本。 failure = Some(error); apsis_ephemeris::Sample::new( seconds, @@ -191,7 +255,7 @@ impl Orbit { /// Applies a finite burn and returns the resulting osculating elements. /// - /// The burn is applied to the osculating state at the given time, so the + /// The burn is applied to the oscillating state at the given time, so the /// manoeuvre is described relative to the orbit the satellite is /// actually on rather than the mean elements. The resulting elements are /// the osculating elements of the post burn state, which is what a @@ -199,6 +263,15 @@ impl Orbit { /// built for one element set, this is a planning result rather than a /// new propagator; a caller that wants to propagate the new orbit starts /// a fresh `Orbit` from the returned elements. + /// + /// 施加有限推力点火并返回所得的密切根数。 + /// + /// 点火施加于给定时刻的密切状态,故该机动是相对卫星实际所在轨道而非平均 + /// 根数描述的。所得根数是点火后状态的密切根数,正是后续传播所需的。由于 + /// 底层传播器是为一个根数集构建的,这给出的是规划结果而非新的传播器; + /// 需要传播新轨道的调用方应据此创建新的 `Orbit`。 + /// + /// 持续时间可由 30 天星历采样路径支撑。 pub fn apply_burn( &self, minutes: f64, @@ -221,16 +294,24 @@ impl Orbit { } /// A failure while sampling an orbit into an ephemeris. +/// +/// 把轨道采样为星历时发生的失败。 #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum EphemerisSamplingError { /// The requested grid is not usable: the step is not positive or the /// range is empty or reversed. + /// + /// 请求的网格不可用:步长不为正,或区间为空或倒置。 InvalidGrid, /// A sample could not be propagated. + /// + /// 某样本无法传播。 Propagation(PropagationError), /// The table rejected the samples, which should not happen for a valid /// grid and is reported rather than hidden. + /// + /// 表格拒绝了这些样本;对有效网格这不应发生,故予以报告而非隐藏。 Ephemeris(EphemerisError), } @@ -247,15 +328,23 @@ impl fmt::Display for EphemerisSamplingError { impl core::error::Error for EphemerisSamplingError {} /// A failure while applying a burn to an orbit. +/// +/// 把点火作用于轨道时发生的失败。 #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum ManeuverApplicationError { /// The state before the burn could not be propagated. + /// + /// 点火前的状态无法传播。 Propagation(PropagationError), /// The burn itself was not usable. + /// + /// 点火本身不可用。 Maneuver(ManeuverError), /// The post burn state is not an elliptical orbit, which happens when /// the burn is large enough to escape. + /// + /// 点火后状态不是椭圆轨道,这发生于点火足够大以致逃逸时。 NotElliptical, } @@ -277,10 +366,17 @@ impl core::error::Error for ManeuverApplicationError {} /// parameter, and using the wrong one shifts the osculating semi-major axis /// by a small amount. The values are taken from the model definitions rather /// than duplicated, so a change to a model propagates here. +/// +/// 返回与某重力模型匹配的引力参数。 +/// +/// 两种模型的引力参数值略有差异,用错会使密切半长轴发生小幅偏移。这些值取自 +/// 模型定义而非重复抄写,故模型的改动会传播到这里。 fn parameter_for(model: GravityModel) -> GravityParameter { // The two parameters differ by 0.3 km^3/s^2, about one part in a // million. The comparison is on the model's defining value, which is // exact for the constants defined in this workspace. + // 两种参数相差 0.3 km^3/s^2,约为百万分之一。比较基于模型定义值,对本 + // 工作区定义的常量是精确的。 if model.mu == GravityModel::WGS84.mu { GravityParameter::WGS84 } else { diff --git a/crates/orbit/apsis-sgp4/src/batch.rs b/crates/orbit/apsis-sgp4/src/batch.rs index 9197936..39bcfe6 100644 --- a/crates/orbit/apsis-sgp4/src/batch.rs +++ b/crates/orbit/apsis-sgp4/src/batch.rs @@ -16,6 +16,10 @@ use apsis_math::parallel::map_index; /// /// The sequential form, provided for comparison and for small lists where /// starting a thread would cost more than the work. +/// +/// 按顺序在历元后若干分钟列表处传播该卫星。 +/// +/// 顺序形式,供比较以及线程开销高于工作时的小列表使用。 pub fn propagate_minutes_at(satellite: &Satellite, minutes: &[f64]) -> Vec> { minutes .iter() @@ -41,6 +45,19 @@ pub fn propagate_minutes_at(satellite: &Satellite, minutes: &[f64]) -> Vec Vec> { map_index(minutes.len(), |index| { satellite.propagate_minutes(minutes[index]).ok() @@ -52,6 +69,11 @@ pub fn propagate_minutes_at_parallel(satellite: &Satellite, minutes: &[f64]) -> /// The per satellite work is independent, so a shell's state at a single /// instant is a natural batch. The satellites are read only, so they are /// shared across the threads by reference. +/// +/// 按顺序在同一瞬时传播多颗卫星。 +/// +/// 各卫星的工作彼此独立,故整壳在同一瞬时的状态是天然的批处理单元。卫星 +/// 只读,因此在各线程间按引用共享。 pub fn propagate_at(satellites: &[Satellite], tai_mjd: f64) -> Vec> { satellites .iter() @@ -67,6 +89,13 @@ pub fn propagate_at(satellites: &[Satellite], tai_mjd: f64) -> Vec /// This is the form a coverage run wants: it holds the whole shell in one /// call, so the threads start once for every satellite rather than once per /// satellite, and even a shell of a few hundred gains. +/// +/// 跨多个线程在同一瞬时传播多颗卫星。 +/// +/// 结果与 [`propagate_at`] 逐元素相同且顺序一致。 +/// +/// 覆盖计算需要的形式:它一次调用持有整壳,故线程为所有卫星启动一次而非 +/// 每颗一次,即便几百颗的壳也能获益。 pub fn propagate_at_parallel(satellites: &[Satellite], tai_mjd: f64) -> Vec> { map_index(satellites.len(), |index| { satellites[index].propagate_at(tai_mjd).ok() diff --git a/crates/orbit/apsis-sgp4/src/deep_space.rs b/crates/orbit/apsis-sgp4/src/deep_space.rs index c05f33b..1111fd6 100644 --- a/crates/orbit/apsis-sgp4/src/deep_space.rs +++ b/crates/orbit/apsis-sgp4/src/deep_space.rs @@ -24,26 +24,26 @@ use crate::satellite::{Satellite, TWO_PI}; use core::f64::consts::PI; -/// Solar eccentricity. +/// 太阳偏心率。 const ZES: f64 = 0.016_75; -/// Lunar eccentricity. +/// 月球偏心率。 const ZEL: f64 = 0.054_90; -/// Solar mean motion coefficient, in radians per minute. +/// 太阳平运动系数,单位为弧度每分钟。 const ZNS: f64 = 1.194_59e-5; -/// Lunar mean motion coefficient, in radians per minute. +/// 月球平运动系数,单位为弧度每分钟。 const ZNL: f64 = 1.583_521_8e-4; /// The rate of rotation of the Earth used by the resonance integral, in /// radians per minute. /// -/// This is `7.292 115 146 688 55e-5` radians per second. +/// 该值为 `7.292 115 146 688 55e-5` 弧度每秒。 const RPTIM: f64 = 4.375_269_088_011_3e-3; -/// The integration step of the resonance, in minutes. +/// 共振积分的积分步长,单位为分钟。 const STEP_POSITIVE: f64 = 720.0; -/// The integration step of the resonance for backward propagation. +/// 向后传播时共振积分的积分步长。 const STEP_NEGATIVE: f64 = -720.0; -/// The square of the integration step, in minutes squared. +/// 积分步长的平方,单位为分钟每二次方。 const STEP_SQUARED: f64 = 259_200.0; /// The values returned by the first stage of the deep space @@ -54,93 +54,105 @@ const STEP_SQUARED: f64 = 259_200.0; /// with a doubled letter, and the values from the second pass, for the Sun, /// keep the single letter. The final coefficients combine one from each /// group, so both are carried here. +/// +/// 深空初始化第一阶段返回的值,该阶段计算日月级数。 +/// +/// 参考实现以首字母 `s` 命名对该级数的两次求值:第一遍(月球)的值用双写 +/// 字母存放,第二遍(太阳)的值保留单字母。最终系数各取其一组合,故两者 +/// 都在此保留。 #[derive(Debug, Clone, Copy)] struct Lunisolar { - /// Moon series, second order. + /// 月球级数,二阶项。 ss1: f64, - /// Moon series, second order. + /// 月球级数,二阶项。 ss2: f64, - /// Moon series, second order. + /// 月球级数,二阶项。 ss3: f64, - /// Moon series, second order. + /// 月球级数,二阶项。 ss4: f64, - /// Moon series, second order. + /// 月球级数,二阶项。 ss5: f64, - /// Moon series, second order. + /// 月球级数,二阶项。 ss6: f64, - /// Moon series, second order. + /// 月球级数,二阶项。 ss7: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz1: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz2: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz3: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz11: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz12: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz13: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz21: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz22: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz23: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz31: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz32: f64, - /// Moon series, radial terms. + /// 月球级数,径向项。 sz33: f64, - /// Sun series, second order. + /// 太阳级数,二阶项。 s1: f64, - /// Sun series, second order. + /// 太阳级数,二阶项。 s2: f64, - /// Sun series, second order. + /// 太阳级数,二阶项。 s3: f64, - /// Sun series, second order. + /// 太阳级数,二阶项。 s4: f64, - /// Sun series, second order. + /// 太阳级数,二阶项。 s5: f64, - /// Sun series, second order. + /// 太阳级数,二阶项。 s6: f64, - /// Sun series, second order. + /// 太阳级数,二阶项。 s7: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z1: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z2: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z3: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z11: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z12: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z13: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z21: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z22: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z23: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z31: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z32: f64, - /// Sun series, radial terms. + /// 太阳级数,径向项。 z33: f64, - /// Mean anomaly of the Moon at the epoch, in radians. + /// 历元处月球平近点角,单位为弧度。 zmol: f64, - /// Mean anomaly of the Sun at the epoch, in radians. + /// 历元处太阳平近点角,单位为弧度。 zmos: f64, /// Eccentricity squared at the epoch. + /// + /// 历元处的偏心率平方。 emsq: f64, /// Sine of the inclination at the epoch. + /// + /// 历元处倾角的正弦。 sinim: f64, /// Cosine of the inclination at the epoch. + /// + /// 历元处倾角的余弦。 cosim: f64, } @@ -149,118 +161,141 @@ struct Lunisolar { /// Every field is zero for a near-Earth orbit, so the structure can be /// stored unconditionally and the near-Earth propagation step pays only the /// cost of carrying the storage. +/// +/// 卫星的深空系数。 +/// +/// 对近地轨道每个字段都为零,故该结构可无条件存储,而近地传播步骤只承担 +/// 携带该存储的代价。 #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct DeepSpace { - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) e3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) ee2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) se2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) se3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) si2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) si3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) sl2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) sl3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) sl4: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) sgh2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) sgh3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) sgh4: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) sh2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) sh3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xgh2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xgh3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xgh4: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xh2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xh3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xi2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xi3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xl2: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xl3: f64, - /// Solar and lunar long period periodic coefficients. + /// 日月长周期项系数。 pub(crate) xl4: f64, - /// Mean anomaly of the Moon at the epoch, in radians. + /// 历元处月球平近点角,单位为弧度。 pub(crate) zmol: f64, - /// Mean anomaly of the Sun at the epoch, in radians. + /// 历元处太阳平近点角,单位为弧度。 pub(crate) zmos: f64, - /// The long period periodic terms carried forward from the epoch. + /// 自历元沿用的长周期项。 pub(crate) peo: f64, - /// The long period periodic terms carried forward from the epoch. + /// 自历元沿用的长周期项。 pub(crate) pinco: f64, - /// The long period periodic terms carried forward from the epoch. + /// 自历元沿用的长周期项。 pub(crate) plo: f64, - /// The long period periodic terms carried forward from the epoch. + /// 自历元沿用的长周期项。 pub(crate) pgho: f64, - /// The long period periodic terms carried forward from the epoch. + /// 自历元沿用的长周期项。 pub(crate) pho: f64, /// Resonance flag: zero for none, one for the synchronous resonance, two /// for the half-day resonance. + /// + /// 共振标志:零表示无共振,一表示同步共振,二表示半日共振。 pub(crate) irez: i32, /// Secular rate of the eccentricity, per minute. + /// + /// 偏心率的长期变化率,每分钟。 pub(crate) dedt: f64, /// Secular rate of the inclination, per minute. + /// + /// 倾角的长期变化率,每分钟。 pub(crate) didt: f64, /// Secular rate of the mean anomaly, per minute. + /// + /// 平近点角的长期变化率,每分钟。 pub(crate) dmdt: f64, /// Secular rate of the right ascension, per minute. + /// + /// 赤经的长期变化率,每分钟。 pub(crate) dnodt: f64, /// Secular rate of the argument of perigee, per minute. + /// + /// 近地点幅角的长期变化率,每分钟。 pub(crate) domdt: f64, - /// Synchronous resonance coefficient. + /// 同步共振系数。 pub(crate) del1: f64, - /// Synchronous resonance coefficient. + /// 同步共振系数。 pub(crate) del2: f64, - /// Synchronous resonance coefficient. + /// 同步共振系数。 pub(crate) del3: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d2201: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d2211: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d3210: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d3222: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d4410: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d4422: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d5220: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d5232: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d5421: f64, - /// Half-day resonance coefficients. + /// 半日共振系数。 pub(crate) d5433: f64, /// Resonance angle rate. + /// + /// 共振角变化率。 pub(crate) xfact: f64, /// Resonance angle at the epoch, in radians. + /// + /// 历元处的共振角,单位为弧度。 pub(crate) xlamo: f64, } impl DeepSpace { /// The all zero value used by near-Earth orbits. + /// + /// 近地轨道所用的全零取值。 pub(crate) const ZERO: Self = Self { e3: 0.0, ee2: 0.0, @@ -318,44 +353,58 @@ impl DeepSpace { } /// The result of applying the long period periodic perturbations. +/// +/// 施加长周期项后的结果。 #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct PerturbedElements { - /// Eccentricity, dimensionless. + /// 偏心率,无量纲。 pub(crate) eccentricity: f64, - /// Inclination, in radians. + /// 倾角,单位为弧度。 pub(crate) inclination: f64, - /// Right ascension of the ascending node, in radians. + /// 升交点赤经,单位为弧度。 pub(crate) node: f64, - /// Argument of perigee, in radians. + /// 近地点幅角,单位为弧度。 pub(crate) argument: f64, - /// Mean anomaly, in radians. + /// 平近点角,单位为弧度。 pub(crate) mean_anomaly: f64, } /// The mean elements after the deep space secular and resonance terms. +/// +/// 施加深空长期项与共振项之后的平均根数。 #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct DeepSpaceState { - /// Eccentricity, dimensionless. + /// 偏心率,无量纲。 pub(crate) eccentricity: f64, - /// Argument of perigee, in radians. + /// 近地点幅角,单位为弧度。 pub(crate) argument: f64, - /// Inclination, in radians. + /// 倾角,单位为弧度。 pub(crate) inclination: f64, - /// Mean anomaly, in radians. + /// 平近点角,单位为弧度。 pub(crate) mean_anomaly: f64, - /// Right ascension of the ascending node, in radians. + /// 升交点赤经,单位为弧度。 pub(crate) node: f64, /// Mean motion, in radians per minute. + /// + /// 平运动,单位为弧度每分钟。 pub(crate) mean_motion: f64, /// The angle of the resonance integrator at the propagated time. + /// + /// 传播时刻共振积分器的角度。 pub(crate) atime: f64, /// The resonance angle at the propagated time, in radians. + /// + /// 传播时刻的共振角,单位为弧度。 pub(crate) xli: f64, /// The resonance mean motion at the propagated time. + /// + /// 传播时刻的共振平运动。 pub(crate) xni: f64, } /// Reduces a value into `[0, 2 pi)` by taking the positive remainder. +/// +/// 取正余数把某个值归约到 `[0, 2 pi)`。 fn remainder_positive(value: f64) -> f64 { value.rem_euclid(TWO_PI) } @@ -365,6 +414,11 @@ fn remainder_positive(value: f64) -> f64 { /// The reference uses this convention for the right ascension, where a /// negative result is meaningful, and it differs from a positive remainder /// for negative inputs. +/// +/// 归约某个值并保留被除数的符号。 +/// +/// 参考实现对赤经采用此约定,因为负结果在此有意义;对负输入,它与取正余数 +/// 的结果不同。 fn remainder_signed(value: f64) -> f64 { if value >= 0.0 { value % TWO_PI @@ -374,6 +428,8 @@ fn remainder_signed(value: f64) -> f64 { } /// Computes the deep space coefficients for a satellite. +/// +/// 计算某卫星的深空系数。 pub(crate) fn initialise(satellite: &Satellite) -> DeepSpace { let lunisolar = lunisolar_coefficients(satellite); let mut deep = DeepSpace::ZERO; @@ -416,6 +472,11 @@ pub(crate) fn initialise(satellite: &Satellite) -> DeepSpace { /// The series is evaluated twice with different orientations, once aligned /// with the Moon and once with the Sun. Both sets of results are needed /// because the final coefficients pair the two. +/// +/// 在历元处求值日月级数。 +/// +/// 该级数以两种不同的取向各求值一次,一次与月球对齐,一次与太阳对齐。 +/// 两组结果都需要,因为最终系数把二者配对使用。 fn lunisolar_coefficients(satellite: &Satellite) -> Lunisolar { const C1SS: f64 = 2.986_479_7e-6; const C1L: f64 = 4.796_806_5e-7; @@ -428,6 +489,8 @@ fn lunisolar_coefficients(satellite: &Satellite) -> Lunisolar { // Days counted from the start of 1900. The reference adds 18 261.5 to a // day count from 1950, which is the same origin. + // 自 1900 年初起计的天数。参考实现在自 1950 年的日计数上加 18 261.5, + // 二者原点相同。 let day = satellite.epoch_mjd - 15_019.5; let emsq = ep * ep; @@ -464,6 +527,7 @@ fn lunisolar_coefficients(satellite: &Satellite) -> Lunisolar { let xnoi = 1.0 / np; // Values from the first pass, for the Moon. + // 第一遍的值,对应月球。 let (mut ss1, mut ss2, mut ss3, mut ss4, mut ss5, mut ss6, mut ss7) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); let ( @@ -482,6 +546,7 @@ fn lunisolar_coefficients(satellite: &Satellite) -> Lunisolar { ) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); // Values from the second pass, for the Sun. + // 第二遍的值,对应太阳。 let (mut s1, mut s2, mut s3, mut s4, mut s5, mut s6, mut s7) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); let ( @@ -536,6 +601,7 @@ fn lunisolar_coefficients(satellite: &Satellite) -> Lunisolar { let z23p = 6.0 * a4 * a6 + emsq * (24.0 * x2 * x6 - 6.0 * x4 * x8); // The reference doubles each of these and adds the sub-expression // multiplied by `1 - e^2`. + // 参考实现把各项乘以二,再加上该子表达式与 `1 - e^2` 的乘积。 z1p = z1p + z1p + betasq * z31p; z2p = z2p + z2p + betasq * z32p; z3p = z3p + z3p + betasq * z33p; @@ -647,10 +713,14 @@ fn lunisolar_coefficients(satellite: &Satellite) -> Lunisolar { } /// Computes the secular rates of the mean elements. +/// +/// 计算平均根数的长期变化率。 fn initialise_rates(satellite: &Satellite, ls: &Lunisolar, deep: &mut DeepSpace) { // The reference sets `shs` and `shll` to zero for inclinations within // about 3 degrees of the equator or the pole, where the division by the // sine of the inclination is ill conditioned. + // 倾角在赤道或极点附近约 3 度以内时,参考实现把 `shs` 与 `shll` 置零, + // 因为那里除以倾角正弦是病态的。 let inclined = (5.235_987_7e-2..=PI - 5.235_987_7e-2).contains(&satellite.inclo); let ses = ls.ss1 * ZNS * ls.ss5; @@ -686,6 +756,8 @@ fn initialise_rates(satellite: &Satellite, ls: &Lunisolar, deep: &mut DeepSpace) } /// Computes the resonance coefficients. +/// +/// 计算共振系数。 fn initialise_resonance(satellite: &Satellite, ls: &Lunisolar, deep: &mut DeepSpace) { const Q22: f64 = 1.789_167_9e-6; const Q31: f64 = 2.146_074_8e-6; @@ -709,6 +781,8 @@ fn initialise_resonance(satellite: &Satellite, ls: &Lunisolar, deep: &mut DeepSp // The synchronous resonance applies near one revolution per sidereal // day and the half-day resonance near two, with a further restriction on // the eccentricity. + // 同步共振适用于每恒星日约一圈处,半日共振适用于约两圈处,并对偏心率 + // 另有约束。 let mut irez = 0; if 0.003_490_658_5 < nm && nm < 0.005_235_987_7 { irez = 1; @@ -728,6 +802,7 @@ fn initialise_resonance(satellite: &Satellite, ls: &Lunisolar, deep: &mut DeepSp // Half-day resonance. The eccentricity series is evaluated at the // eccentricity from the element set, not the value carried through // the secular rates. + // 半日共振。偏心率级数取 let cosisq = cosim * cosim; let emo = em; let em = satellite.ecco; @@ -824,6 +899,7 @@ fn initialise_resonance(satellite: &Satellite, ls: &Lunisolar, deep: &mut DeepSp let _ = (emo, emsqo); } else { // Synchronous resonance. + // 同步共振。 let g200 = 1.0 + emsq * (-2.5 + 0.8125 * emsq); let g310 = 1.0 + 2.0 * emsq; let g300 = 1.0 + emsq * (-6.0 + 6.609_37 * emsq); @@ -849,6 +925,8 @@ fn initialise_resonance(satellite: &Satellite, ls: &Lunisolar, deep: &mut DeepSp } /// Applies the deep space secular and resonance terms to the mean elements. +/// +/// 把深空长期项与共振项施加到平均根数上。 #[allow(clippy::too_many_arguments)] pub(crate) fn dspace( satellite: &Satellite, @@ -877,6 +955,9 @@ pub(crate) fn dspace( // the state between calls keeps propagation free of hidden state, and // the result is the same because the step boundaries do not depend on // the requested time. + // 共振以自历元起算的固定 720 分钟步长积分。每次调用都从历元重新开始而 + // 不在调用之间传递状态,使传播不含隐藏状态;结果相同,因为步长边界与 + // 所请求的时刻无关。 let mut atime = 0.0; let mut xli = deep.xlamo; let mut xni = no; @@ -943,6 +1024,11 @@ pub(crate) fn dspace( /// The two resonance cases use different series. The synchronous case, /// `irez == 1`, uses three terms that depend only on the angle, while the /// half-day case uses ten terms that also depend on the argument of perigee. +/// +/// 返回共振角的一阶与二阶导数。 +/// +/// 两种共振情形使用不同的级数。同步情形(`irez == 1`)使用只依赖该角的 +/// 三项,而半日情形使用同时依赖近地点幅角的十项。 fn resonance_derivatives( deep: &DeepSpace, argpo: f64, @@ -1004,6 +1090,11 @@ fn resonance_derivatives( /// requested time, except during initialisation, where they are evaluated at /// the epoch and stored as the reference values that later evaluations /// subtract. +/// +/// 施加深空长周期项。 +/// +/// 各项在所请求时刻的太阳与月球平近点角上求值;初始化阶段除外,那是在历元 +/// 处求值并存为参考基准,供此后的求值相减。 #[allow(clippy::too_many_arguments)] pub(crate) fn dpper( satellite: &Satellite, @@ -1072,6 +1163,8 @@ pub(crate) fn dpper( // node and the argument of perigee are individually ill conditioned. // The threshold of 0.2 radians is about 11.5 degrees. The reference // applies it against the perturbed inclination. + // Lyddane 修正在低倾角处使用,那里交点与近地点幅角各自病态。阈值 + // 0.2 弧度约为 11.5 度。参考实现以受扰动后的倾角作判据。 if inclp >= 0.2 { ph /= sinip; pgh -= cosip * ph; diff --git a/crates/orbit/apsis-sgp4/src/error.rs b/crates/orbit/apsis-sgp4/src/error.rs index 1d556a6..429175f 100644 --- a/crates/orbit/apsis-sgp4/src/error.rs +++ b/crates/orbit/apsis-sgp4/src/error.rs @@ -3,17 +3,26 @@ use core::fmt; /// The category of a propagation failure. +/// +/// 传播失败的类别。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum PropagationErrorKind { /// The mean elements do not describe a satellite the model can /// propagate. This includes an eccentricity at or above one, which is /// not a closed orbit, and a mean motion that is not positive. + /// + /// 平均根数不描述模型可传播的卫星。这包括偏心率不小于一(非闭合轨道) + /// 与平运动不为正的情形。 InvalidElements, /// The satellite has decayed, meaning the drag term has brought it /// below the surface of the Earth. + /// + /// 卫星已衰减,即阻力项把它带到地表以下。 Decayed, /// The propagation left the region where the model is defined. + /// + /// 传播超出了模型定义的范围。 OutOfRange, } @@ -29,6 +38,8 @@ impl fmt::Display for PropagationErrorKind { } /// A failure while initialising or propagating a satellite. +/// +/// 初始化或传播卫星时发生的失败。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PropagationError { kind: PropagationErrorKind, @@ -37,22 +48,30 @@ pub struct PropagationError { impl PropagationError { /// Creates an error with the given category. + /// + /// 用给定类别创建一个错误。 pub fn new(kind: PropagationErrorKind) -> Self { Self { kind, detail: None } } /// Attaches a description of the value that caused the failure. + /// + /// 附加对该失败所涉数值的说明。 pub fn with_detail(mut self, detail: impl Into) -> Self { self.detail = Some(detail.into()); self } /// Returns the category of this failure. + /// + /// 返回该失败的类别。 pub fn kind(&self) -> PropagationErrorKind { self.kind } /// Returns the detail text attached to this error, if any. + /// + /// 返回附加在该错误上的说明文本,若有。 pub fn detail(&self) -> Option<&str> { self.detail.as_deref() } diff --git a/crates/orbit/apsis-sgp4/src/init.rs b/crates/orbit/apsis-sgp4/src/init.rs index 4788129..6f32eb5 100644 --- a/crates/orbit/apsis-sgp4/src/init.rs +++ b/crates/orbit/apsis-sgp4/src/init.rs @@ -20,39 +20,68 @@ use apsis_tle::TwoLineElement; use core::f64::consts::PI; /// The number of minutes in a day. +/// +/// 一天中的分钟数。 const MINUTES_PER_DAY: f64 = 1_440.0; /// The number of radians in a revolution. +/// +/// 一周中的弧度数。 const TWO_PI: f64 = 2.0 * PI; /// The period, in minutes, at or above which the deep space branch is used. /// /// The theory states 225 minutes. Satellites below that period are handled /// by the near-Earth branch, which is the case for all low Earth orbits. +/// +/// 使用深空分支的周期阈值,单位为分钟。 +/// +/// 理论给出 225 分钟。周期短于该值的卫星由近地分支处理,所有低地球轨道皆属此类。 const DEEP_SPACE_PERIOD_MINUTES: f64 = 225.0; /// The elements of a satellite in the units the propagator uses. /// /// Angles are in radians and mean motion is in radians per minute. Helpers /// convert a [`TwoLineElement`] into this form. +/// +/// 卫星根数在传播器所用单位下的形式。 +/// +/// 角度用弧度,平运动用弧度每分钟。辅助函数把 [`TwoLineElement`] 转为该形式。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct Elements { /// Epoch of the elements, expressed as a Modified Julian Date on the /// scale the element set was written in. + /// + /// 根数历元,以根数集书写所用标度上的修正儒略日(Modified Julian Date) + /// 表示。 pub epoch_mjd: f64, /// `BSTAR` drag term, in inverse Earth radii. + /// + /// `BSTAR` 阻力项,单位为地球半径的倒数。 pub bstar: f64, /// Mean motion, in radians per minute. + /// + /// 平运动,单位为弧度每分钟。 pub mean_motion: f64, /// Eccentricity, dimensionless. + /// + /// 偏心率,无量纲。 pub eccentricity: f64, /// Inclination, in radians. + /// + /// 倾角,单位为弧度。 pub inclination: f64, /// Right ascension of the ascending node, in radians. + /// + /// 升交点赤经,单位为弧度。 pub right_ascension: f64, /// Argument of perigee, in radians. + /// + /// 近地点幅角,单位为弧度。 pub argument_of_perigee: f64, /// Mean anomaly, in radians. + /// + /// 平近点角,单位为弧度。 pub mean_anomaly: f64, } @@ -66,6 +95,13 @@ impl Elements { /// scale first would shift the epoch by the leap second offset, about /// 33 seconds, and that shift propagates into the solar and lunar series /// of the deep space branch. + /// + /// 把已解析的根数集转换为传播器所用单位。 + /// + /// 历元直接取根数集中原样写下的 UTC 修正儒略日(Modified Julian Date)。 + /// 根数集历元是 UTC 日历瞬时,但传播器把它当作均匀时间原点,这正是理论 + /// 拟合所依据的约定。若先换算到均匀标度,历元会偏移一个闰秒量(约 33 + /// 秒),而该偏移会传播进深空分支的日月级数。 pub fn from_tle(element: &TwoLineElement) -> Option { let epoch_mjd = element.epoch.mjd(apsis_time::TimeScale::Utc)?; Some(Self { @@ -116,6 +152,26 @@ impl Elements { /// /// Source: Vallado et al., *Revisiting Spacetrack Report #3*, AIAA /// 2006-6753, 2006. +/// +/// 施加从 Kozai 平运动到 Brouwer 平运动的修正。 +/// +/// 根数集中的平运动是 Kozai 平运动,理论须先把它换算为 Brouwer 平运动方能 +/// 使用。以 `no` 表示根数集给出的值、`xke` 表示开普勒常数,参考实现按下式 +/// 求值: +/// +/// ```text +/// ak = (xke / no)^(2/3) +/// d1 = 0.75 j2 (3 cos^2 i - 1) / (sqrt(1 - e^2) (1 - e^2)) +/// del = d1 / ak^2 +/// adel = ak (1 - del^2 - del (1/3 + 134 del^2 / 81)) +/// del = d1 / adel^2 +/// n0 = no / (1 + del) +/// ``` +/// +/// 第二次求 `del` 用的是 `adel` 而非 `ak`,这正是该修正与修正后平运动所隐含 +/// 的半长轴相一致的原因。 +/// +/// 出处:Vallado 等,*Revisiting Spacetrack Report #3*,AIAA 2006-6753,2006。 pub fn unkozai_mean_motion( mean_motion: f64, eccentricity: f64, @@ -142,6 +198,10 @@ pub fn unkozai_mean_motion( /// Returns an error when the orbit cannot be propagated, for example when /// the eccentricity does not describe a closed orbit or the mean motion is /// not positive. +/// +/// 在初始化前校验平均根数。 +/// +/// 当轨道不可传播时返回错误,例如偏心率不描述闭合轨道,或平运动不为正。 pub fn validate(elements: &Elements) -> Result<(), PropagationError> { if !elements.mean_motion.is_finite() || elements.mean_motion <= 0.0 { return Err(PropagationError::new(PropagationErrorKind::InvalidElements) @@ -175,6 +235,10 @@ pub fn validate(elements: &Elements) -> Result<(), PropagationError> { /// Returns `true` when the elements describe a deep space orbit. /// /// The test is on the orbital period, which follows from the mean motion. +/// +/// 当这些根数描述一条深空轨道时返回 `true`。 +/// +/// 判据是轨道周期,它由平运动推出。 pub fn is_deep_space(mean_motion: f64) -> bool { TWO_PI / mean_motion >= DEEP_SPACE_PERIOD_MINUTES } diff --git a/crates/orbit/apsis-sgp4/src/model.rs b/crates/orbit/apsis-sgp4/src/model.rs index 6e65c7c..ad5b091 100644 --- a/crates/orbit/apsis-sgp4/src/model.rs +++ b/crates/orbit/apsis-sgp4/src/model.rs @@ -22,17 +22,29 @@ //! - WGS 84 values: NIMA TR8350.2, third edition, 2000. /// A gravity model: the Earth constants used by the propagator. +/// +/// 重力模型:传播器所用的地球常数。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct GravityModel { /// Gravitational parameter, in cubic kilometers per second squared. + /// + /// 引力参数,单位为立方千米每二次方秒。 pub mu: f64, /// Equatorial radius, in kilometers. + /// + /// 赤道半径,单位为千米。 pub radius: f64, /// Second zonal harmonic, dimensionless. + /// + /// 第二带谐,无量纲。 pub j2: f64, /// Third zonal harmonic, dimensionless. + /// + /// 第三带谐,无量纲。 pub j3: f64, /// Fourth zonal harmonic, dimensionless. + /// + /// 第四带谐,无量纲。 pub j4: f64, } @@ -41,6 +53,10 @@ impl GravityModel { /// /// This is the model the SGP4 theory was fitted to and the one that /// matches the standard element sets. + /// + /// WGS 72 重力模型。 + /// + /// 这是 SGP4 理论拟合所依据、且与标准根数集相符的模型。 pub const WGS72: Self = Self { mu: 398_600.8, radius: 6_378.135, @@ -52,6 +68,10 @@ impl GravityModel { /// The WGS 84 gravity model. /// /// Use this only with element sets fitted against WGS 84. + /// + /// WGS 84 重力模型。 + /// + /// 仅对按 WGS 84 拟合的根数集使用。 pub const WGS84: Self = Self { mu: 398_600.5, radius: 6_378.137, @@ -62,12 +82,16 @@ impl GravityModel { /// Returns the reciprocal of the square root of the gravitational /// parameter, in seconds per kilometer to the three halves. + /// + /// 返回引力参数平方根的倒数,单位为秒每千米的三分之二次方。 pub fn reciprocal_sqrt_mu(self) -> f64 { 1.0 / self.mu.sqrt() } /// Returns the square root of the gravitational parameter, in kilometers /// to the three halves per second. + /// + /// 返回引力参数的平方根,单位为千米的三分之二次方每秒。 pub fn sqrt_mu(self) -> f64 { self.mu.sqrt() } @@ -83,6 +107,16 @@ impl GravityModel { /// /// which is the angular rate, in radians per minute, of a circular orbit /// of radius one Earth radius. + /// + /// 返回 `xke`(开普勒常数),单位为 `er^1.5 / min`。 + /// + /// 传播器以分钟与地球半径为单位,故引力参数按下式换算: + /// + /// ```text + /// xke = 60 / sqrt(radius^3 / mu) + /// ``` + /// + /// 它是一个地球半径圆轨道的角速率,单位为弧度每分钟。 pub fn xke(self) -> f64 { 60.0 / (self.radius * self.radius * self.radius / self.mu).sqrt() } @@ -91,11 +125,17 @@ impl GravityModel { /// /// This is the time unit of the model: the period of the reference /// circular orbit divided by two pi. + /// + /// 返回 `xke` 的倒数,单位为分钟。 + /// + /// 它是模型的时间单位:参考圆轨道的周期除以二 pi。 pub fn tumin(self) -> f64 { 1.0 / self.xke() } /// Returns `j3 / j2`, which appears as a single ratio in the theory. + /// + /// 返回 `j3 / j2`,理论中以单一比值形式出现。 pub fn j3_over_j2(self) -> f64 { self.j3 / self.j2 } @@ -106,6 +146,11 @@ impl GravityModel { /// It appears as a power rather than a factor, in terms such as /// `(xke / mean_motion)^(2/3)`, so it is a constant and does not depend /// on this model. + /// + /// 返回 `2 / 3`,即模型在平运动与阻力修正中使用的指数。 + /// + /// 它以幂而非因子出现,例如 `(xke / mean_motion)^(2/3)`,故为常数,与 + /// 本模型无关。 pub fn x2o3(self) -> f64 { 2.0 / 3.0 } diff --git a/crates/orbit/apsis-sgp4/src/propagate.rs b/crates/orbit/apsis-sgp4/src/propagate.rs index 1aea8a0..ecfcac1 100644 --- a/crates/orbit/apsis-sgp4/src/propagate.rs +++ b/crates/orbit/apsis-sgp4/src/propagate.rs @@ -21,27 +21,38 @@ use crate::state::State; use apsis_math::Vec3; /// The number of iterations allowed when solving for the eccentric anomaly. +/// +/// 求解偏近点角时允许的迭代次数。 const KEPLER_MAX_ITERATIONS: usize = 10; /// The convergence threshold on the eccentric anomaly correction. +/// +/// 偏近点角修正的收敛阈值。 const KEPLER_TOLERANCE: f64 = 1.0e-12; /// The largest correction applied in one Kepler step. /// /// Clamping keeps the iteration stable for the eccentric orbits that the /// near circular approximation of the theory handles poorly. +/// +/// 设上限使迭代在理论中近圆近似处理不佳的高偏心轨道上保持稳定。 const KEPLER_MAX_STEP: f64 = 0.95; /// The smallest eccentricity used after the drag terms are applied. /// /// A value that would round to zero makes several divisions in the short /// period terms singular, so it is clamped to this floor. +/// +/// 会舍入为零的取值会使短周期项中的若干除法奇异,故钳位到此下限。 const MINIMUM_ECCENTRICITY: f64 = 1.0e-6; /// The divisor used when the inclination is close to pi. +/// +/// 倾角接近 pi 时所用的除数。 const INCLINATION_GUARD: f64 = 1.5e-12; /// Propagates a satellite by a number of minutes from its element epoch. +/// 把某卫星自其根数历元起传播若干分钟。 pub(crate) fn propagate(satellite: &Satellite, minutes: f64) -> Result { let mean_anomaly = satellite.mo + satellite.mdot * minutes; let secular_argument = satellite.argpo + satellite.argpdot * minutes; @@ -80,6 +91,7 @@ pub(crate) fn propagate(satellite: &Satellite, minutes: f64) -> Result Result Result= 0.0 { node % TWO_PI } else { @@ -176,6 +191,7 @@ pub(crate) fn propagate(satellite: &Satellite, minutes: f64) -> Result INCLINATION_GUARD { @@ -198,6 +214,7 @@ pub(crate) fn propagate(satellite: &Satellite, minutes: f64) -> Result Option { Self::try_from_element(element, model).ok() } @@ -127,6 +222,8 @@ impl Satellite { /// /// Returns an error describing which element is not propagatable when /// the elements are rejected. + /// + /// 由已解析的根数集初始化卫星。 pub fn try_from_element( element: &TwoLineElement, model: GravityModel, @@ -139,6 +236,8 @@ impl Satellite { } /// Initialises a satellite from mean elements already in model units. + /// + /// 由已按模型单位表示的平均根数初始化卫星。 pub fn try_from_elements( elements: &Elements, model: GravityModel, @@ -149,11 +248,15 @@ impl Satellite { /// Returns the epoch of the elements as a Modified Julian Date on the /// TAI scale. + /// + /// 返回根数历元,为 TAI 标度上的修正儒略日(Modified Julian Date)。 pub fn epoch_mjd(&self) -> f64 { self.epoch_mjd } /// Returns `true` when the deep space branch applies. + /// + /// 当深空分支适用时返回 `true`。 pub fn is_deep_space(&self) -> bool { self.method == Method::DeepSpace } @@ -163,6 +266,11 @@ impl Satellite { /// /// The instant is given as a Modified Julian Date on the TAI scale, the /// same scale as [`Satellite::epoch_mjd`]. + /// + /// 返回自根数集历元到给定瞬时的分钟数。 + /// + /// 该瞬时以 TAI 标度上的修正儒略日(Modified Julian Date)给出,与 + /// [`Satellite::epoch_mjd`] 同一标度。 pub fn minutes_since_epoch(&self, tai_mjd: f64) -> f64 { (tai_mjd - self.epoch_mjd) * MINUTES_PER_DAY } @@ -170,6 +278,8 @@ impl Satellite { /// Returns the state of the satellite at a given instant. /// /// The instant is a Modified Julian Date on the TAI scale. + /// + /// 返回卫星在某瞬时的状态。 pub fn propagate_at(&self, tai_mjd: f64) -> Result { self.propagate_minutes(self.minutes_since_epoch(tai_mjd)) } @@ -184,12 +294,20 @@ impl Satellite { /// Returns [`PropagationErrorKind::Decayed`](crate::PropagationErrorKind::Decayed) /// when the propagated radius falls below one Earth radius, which the /// model treats as the satellite having re-entered. + /// + /// 返回卫星距根数集历元若干分钟处的状态。 + /// + /// 当传播所得半径低于一个地球半径时返回 + /// [`PropagationErrorKind::Decayed`](crate::PropagationErrorKind::Decayed), + /// 模型将其视为卫星已经再入。 pub fn propagate_minutes(&self, minutes: f64) -> Result { crate::propagate::propagate(self, minutes) } /// Returns the Kepler constant of the model, in radians per minute per /// Earth radius to the three halves. + /// + /// 返回模型的开普勒常数,单位为弧度每分每地球半径的三分之二次方。 pub fn xke(&self) -> f64 { self.model.xke() } @@ -198,6 +316,8 @@ impl Satellite { impl Satellite { /// Builds the record, including every coefficient that depends only on /// the epoch elements. + /// + /// 构建该记录,含所有只依赖历元根数的系数。 fn initialise(elements: &Elements, model: GravityModel) -> Self { let xke = model.xke(); let j2 = model.j2; @@ -232,6 +352,8 @@ impl Satellite { // which the atmosphere is treated as having constant density, and // `qzms24` is the corresponding density scale raised to the fourth // power. + // 空气动力学参数。`sfour` 是把大气视为密度恒定时的有效半径, + // `qzms24` 是相应密度尺度的四次方。 let ss = 78.0 / model.radius + 1.0; let qzms2t = ((120.0 - 78.0) / model.radius).powi(4); @@ -309,6 +431,8 @@ impl Satellite { // The divisor guards the singularity at inclination pi, where the // expression is finite in the limit. `1.5e-12` is the threshold used // by the reference implementation. + // 该除数用于保护倾角为 pi 处的奇点,该处表达式在极限下有限。`1.5e-12` + // 是参考实现所用的阈值。 const INCLINATION_GUARD: f64 = 1.5e-12; let xlcof = if (cosio + 1.0).abs() > INCLINATION_GUARD { -0.25 * model.j3_over_j2() * sinio * (3.0 + 5.0 * cosio) / (1.0 + cosio) @@ -363,6 +487,7 @@ impl Satellite { }; // The higher order drag terms are omitted for the simplified branch. + // 简化分支省略高阶阻力项。 if !isimp { let cc1sq = cc1 * cc1; let d2 = 4.0 * ao * tsi * cc1sq; @@ -396,6 +521,13 @@ impl Satellite { /// /// Source: Vallado et al., *Revisiting Spacetrack Report #3*, AIAA /// 2006-6753, 2006. +/// +/// 返回 UT1 儒略日对应的格林尼治恒星时,单位为弧度。 +/// +/// 该表达式取自参考实现。它展开了世界时与恒星时之比,用其初始化深空共振已足够 +/// 精确,而该值仅在该处使用。 +/// +/// 出处:Vallado 等,*Revisiting Spacetrack Report #3*,AIAA 2006-6753,2006。 pub(crate) fn gstime(julian_date_ut1: f64) -> f64 { let centuries = (julian_date_ut1 - 2_451_545.0) / 36_525.0; let mut seconds = -6.2e-6 * centuries * centuries * centuries @@ -410,6 +542,8 @@ pub(crate) fn gstime(julian_date_ut1: f64) -> f64 { } /// Returns the propagation error for a mean motion that is not positive. +/// +/// 返回平运动不为正时的传播错误。 pub(crate) fn non_positive_mean_motion(mean_motion: f64) -> PropagationError { PropagationError::new(PropagationErrorKind::InvalidElements) .with_detail(format!("propagated mean motion is {mean_motion}")) diff --git a/crates/orbit/apsis-sgp4/src/state.rs b/crates/orbit/apsis-sgp4/src/state.rs index 3ecbb81..d370aa7 100644 --- a/crates/orbit/apsis-sgp4/src/state.rs +++ b/crates/orbit/apsis-sgp4/src/state.rs @@ -9,28 +9,45 @@ use apsis_math::Vec3; /// frame is the one the model defines, with its first axis toward the mean /// equinox, its third axis along the true equator pole, and the second /// completing a right-handed set. +/// +/// 真赤道平春分点系中卫星的位置与速度。 +/// +/// 位置单位为千米,速度单位为千米每秒。该系是模型所定义者:第一轴指向平春分点, +/// 第三轴沿真赤道极,第二轴构成右手系。 #[derive(Debug, Clone, Copy, PartialEq)] pub struct State { /// Position, in kilometers. + /// + /// 位置,单位为千米。 pub position: Vec3, /// Velocity, in kilometers per second. + /// + /// 速度,单位为千米每秒。 pub velocity: Vec3, /// The propagated semi-major axis, in kilometers. /// /// This is the semi-major axis after the secular drag term has been /// applied, so it differs from the value implied by the mean motion in /// the element set. + /// + /// 传播所得的半长轴,单位为千米。 + /// + /// 这是施加长期阻力项之后的半长轴,故与根数集中平运动所隐含的值不同。 pub semi_major_axis: f64, } impl State { /// Returns the distance from the center of the Earth, in kilometers. + /// + /// 返回到地心的距离,单位为千米。 pub fn altitude_radius(self) -> f64 { self.position.norm() } /// Returns the speed relative to the Earth's center, in kilometers per /// second. + /// + /// 返回相对地心的速率,单位为千米每秒。 pub fn speed(self) -> f64 { self.velocity.norm() } diff --git a/crates/orbit/apsis-tle/src/checksum.rs b/crates/orbit/apsis-tle/src/checksum.rs index 9041781..2ae5eb6 100644 --- a/crates/orbit/apsis-tle/src/checksum.rs +++ b/crates/orbit/apsis-tle/src/checksum.rs @@ -18,6 +18,13 @@ use crate::field::{ElementError, ElementErrorKind, Result}; /// /// A minus sign contributes one, a decimal digit contributes its value, and /// every other character contributes nothing. +/// +/// 返回某行前 68 列的校验和。 +/// +/// 输入可以包含末尾的校验和数字,该位被忽略,因为只有前 68 列参与求和。短于 +/// 68 字符的行只贡献其实际具有的列。 +/// +/// 负号贡献 1,十进制数字贡献其数值,其余字符不贡献。 pub fn line_checksum(line: &str) -> u32 { line.chars() .take(68) @@ -39,12 +46,23 @@ pub fn line_checksum(line: &str) -> u32 { /// `line_number` is the one based number used in any error that is /// returned, so that a caller parsing a multi-line input can report which /// line failed. +/// +/// 校验某行末位字符是否为其校验和。 +/// +/// 该行至少须有 69 个字符,因为校验和位于第 69 列。期望值与同一行的 +/// [`line_checksum`] 比较。 +/// +/// `line_number` 是返回错误时使用的从一开始的行号,使解析多行输入的调用方 +/// 能指出是哪一行失败。 pub fn verify_line_checksum(line: &str, line_number: usize) -> Result<()> { let checksum_columns = (69, 69); // The line must contain at least 69 characters. Counting characters // rather than bytes is deliberate: the format is ASCII, but a caller // may hand in text with a non-ASCII character by mistake, and counting // characters gives a clearer failure than slicing through one. + // 该行至少须含 69 个字符。刻意按字符而非字节计数:格式虽为 ASCII,但 + // 调用方可能误传含非 ASCII 字符的文本,按字符计数给出的失败信息比 + // 从多字节字符中间切片更清晰。 let characters: Vec = line.chars().collect(); if characters.len() < 69 { return Err(ElementError::new( diff --git a/crates/orbit/apsis-tle/src/element.rs b/crates/orbit/apsis-tle/src/element.rs index 5b820aa..d713ebe 100644 --- a/crates/orbit/apsis-tle/src/element.rs +++ b/crates/orbit/apsis-tle/src/element.rs @@ -5,18 +5,28 @@ use crate::field::{ElementError, ElementErrorKind, Result}; use apsis_time::{Epoch, TimeScale}; /// The security classification of an element set. +/// +/// 根数集的安全密级。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Classification { /// Unclassified. + /// + /// 未分类。 Unclassified, /// Classified as secret by the originating authority. + /// + /// 由发布机构列为机密。 Secret, /// Classified at some level the parser does not recognise. + /// + /// 列为解析器无法识别的密级。 Other(char), } impl Classification { /// Returns the character used in the element set. + /// + /// 返回根数集中所用的字符。 pub const fn as_char(self) -> char { match self { Self::Unclassified => 'U', @@ -32,15 +42,28 @@ impl Classification { /// written in the element set. Units are the ones the format uses, named in /// each field's documentation. Converting them to a state vector is the /// concern of `apsis-sgp4`. +/// +/// 已解析的两线根数集。 +/// +/// 各字段即根数集中原样写下的平均根数与标识信息。单位沿用格式所用者,在各字段 +/// 文档中注明。把它们转换为状态矢量是 `apsis-sgp4` 的职责。 #[derive(Debug, Clone, PartialEq)] pub struct TwoLineElement { /// Satellite name from the optional first line, if one was present. + /// + /// 可选首行中的卫星名称,若存在。 pub name: Option, /// Satellite catalog number. + /// + /// 卫星编目号。 pub satellite_number: u32, /// Classification of the element set. + /// + /// 根数集的密级。 pub classification: Classification, /// International designator, such as `98067A`. + /// + /// 国际designator,如 `98067A`。 pub international_designator: String, /// Epoch of the mean elements, on the UTC scale. /// @@ -49,41 +72,76 @@ pub struct TwoLineElement { /// `57` through `99` are in the twentieth century and years `00` /// through `56` are in the twenty-first, which places the boundary /// before the start of the satellite catalogue. + /// + /// 平均根数的历元,位于 UTC 标度。 + /// + /// 格式以两位数年份加一日的小数部分存储历元。两位数年份按如下约定展开: + /// `57` 至 `99` 属二十世纪,`00` 至 `56` 属二十一世纪,该分界位于卫星编目 + /// 开始之前。 pub epoch: Epoch, /// First derivative of mean motion divided by two, in revolutions per /// day squared. /// /// The format stores half of the derivative, which is why the field is /// documented as `ndot / 2`. + /// + /// 平运动的一阶导数除以二,单位为每日每二次方圈。 + /// + /// 格式存储该导数的一半,故本字段记作 `ndot / 2`。 pub mean_motion_dot: f64, /// Second derivative of mean motion divided by six, in revolutions per /// day cubed. /// /// The format stores one sixth of the derivative, which is why the /// field is documented as `nddot / 6`. + /// + /// 平运动的二阶导数除以六,单位为每日每三次方圈。 + /// + /// 格式存储该导数的六分之一,故本字段记作 `nddot / 6`。 pub mean_motion_double_dot: f64, /// The `BSTAR` drag term, in units of inverse Earth radii. /// /// The term is a free parameter of the SGP4 drag model. Its units are /// the reciprocal of the reference distance used by the model, which is /// the Earth equatorial radius in kilometers. + /// + /// `BSTAR` 阻力项,单位为地球半径的倒数。 + /// + /// 该项是 SGP4 阻力模型的自由参数。其单位是模型所用参考距离的倒数,该距离 + /// 即以千米表示的地球赤道半径。 pub bstar: f64, /// The element set number, which increments when a satellite's elements /// are refreshed. + /// + /// 根数集编号,在卫星根数更新时递增。 pub element_set_number: u32, /// Orbital inclination, in degrees. + /// + /// 轨道倾角,单位为度。 pub inclination: f64, /// Right ascension of the ascending node, in degrees. + /// + /// 升交点赤经,单位为度。 pub right_ascension: f64, /// Orbital eccentricity, dimensionless. + /// + /// 轨道偏心率,无量纲。 pub eccentricity: f64, /// Argument of perigee, in degrees. + /// + /// 近地点幅角,单位为度。 pub argument_of_perigee: f64, /// Mean anomaly, in degrees. + /// + /// 平近点角,单位为度。 pub mean_anomaly: f64, /// Mean motion, in revolutions per day. + /// + /// 平运动,单位为每日圈数。 pub mean_motion: f64, /// Revolution number at epoch. + /// + /// 历元处的转数。 pub revolution_number: u32, } @@ -92,6 +150,10 @@ pub struct TwoLineElement { /// /// Ranges are inclusive of both ends, matching the column numbering in the /// format definition. +/// +/// 返回某字段的从一开始、含两端的字符区间,以字符切片表示。 +/// +/// 区间含两端,与格式定义中的列编号一致。 fn field(line: &str, first: usize, last: usize) -> Option { if first < 1 || last < first { return None; @@ -104,6 +166,8 @@ fn field(line: &str, first: usize, last: usize) -> Option { } /// Reads a field and returns it trimmed of surrounding spaces. +/// +/// 读取某字段并去除首尾空格。 fn trimmed_field(line: &str, first: usize, last: usize, line_number: usize) -> Result { match field(line, first, last) { Some(text) => Ok(text.trim().to_string()), @@ -116,6 +180,8 @@ fn trimmed_field(line: &str, first: usize, last: usize, line_number: usize) -> R } /// Parses an unsigned integer field. +/// +/// 解析无符号整数字段。 fn unsigned_field(line: &str, first: usize, last: usize, line_number: usize) -> Result { let text = trimmed_field(line, first, last, line_number)?; text.parse::().map_err(|_| { @@ -129,6 +195,8 @@ fn unsigned_field(line: &str, first: usize, last: usize, line_number: usize) -> } /// Parses a floating point field written with an explicit decimal point. +/// +/// 解析带显式小数点的浮点字段。 fn float_field(line: &str, first: usize, last: usize, line_number: usize) -> Result { let text = trimmed_field(line, first, last, line_number)?; text.parse::().map_err(|_| { @@ -149,6 +217,12 @@ fn float_field(line: &str, first: usize, last: usize, line_number: usize) -> Res /// For example ` 12345-3` is `0.12345e-3` and `-11606-4` is `-0.11606e-4`. /// An all blank field is zero, which is how the format writes an absent /// term. +/// +/// 解析形如 `smmmmmse` 的指数字段:首个 `s` 是尾数的可选符号,`e` 是带符号的 +/// 一位指数,尾数按首位之前带隐含小数点解释。 +/// +/// 例如 ` 12345-3` 即 `0.12345e-3`,`-11606-4` 即 `-0.11606e-4`。全空白 +/// 字段为零,这正是格式表示某项缺省的方式。 fn exponent_field(line: &str, first: usize, last: usize, line_number: usize) -> Result { let text = trimmed_field(line, first, last, line_number)?; let columns = Some((first, last)); @@ -158,6 +232,7 @@ fn exponent_field(line: &str, first: usize, last: usize, line_number: usize) -> // The last character is the exponent sign and the one before it is the // exponent magnitude, so the mantissa is everything before those two. + // 末位字符是指数符号,其前一位是指数大小,故尾数为这两位之前的全部内容。 let characters: Vec = text.chars().collect(); if characters.len() < 3 { return Err( @@ -216,6 +291,8 @@ fn exponent_field(line: &str, first: usize, last: usize, line_number: usize) -> /// Parses the eccentricity field, which is written with an assumed decimal /// point before its first digit. +/// +/// 解析偏心率字段,该字段按首位之前带隐含小数点书写。 fn assumed_decimal_field(line: &str, first: usize, last: usize, line_number: usize) -> Result { let text = trimmed_field(line, first, last, line_number)?; let columns = Some((first, last)); @@ -236,6 +313,11 @@ fn assumed_decimal_field(line: &str, first: usize, last: usize, line_number: usi /// /// The boundary sits before the start of the satellite catalogue, so no /// catalogue entry is placed a century away from its true date. +/// +/// 按如下约定展开两位数年份:`57` 至 `99` 属二十世纪,`00` 至 `56` 属 +/// 二十一世纪。 +/// +/// 该分界位于卫星编目开始之前,故不会有编目条目被放到距真实日期一个世纪之处。 pub const fn expand_two_digit_year(two_digit: u32) -> i32 { if two_digit >= 57 { 1900 + two_digit as i32 @@ -246,6 +328,8 @@ pub const fn expand_two_digit_year(two_digit: u32) -> i32 { /// Parses the epoch field, which is a two digit year and a fractional day of /// year, and resolves it on the UTC scale. +/// +/// 解析历元字段,即两位数年份与年内的日小数,并在 UTC 标度上确定其值。 fn epoch_field(line: &str, first: usize, last: usize, line_number: usize) -> Result { let text = trimmed_field(line, first, last, line_number)?; let columns = Some((first, last)); @@ -255,6 +339,7 @@ fn epoch_field(line: &str, first: usize, last: usize, line_number: usize) -> Res // The field is a two digit year followed by three digits of day of year // and then a fraction of that day. + // 该字段为两位数年份,后接三位年日与该日的小数部分。 if text.len() < 5 { return Err(invalid(format!("epoch {text:?} is too short"))); } @@ -284,6 +369,8 @@ fn epoch_field(line: &str, first: usize, last: usize, line_number: usize) -> Res // The field writes the decimal point explicitly, so the fraction // begins with a dot. Strip it before forming the value, otherwise // the parse would see two decimal points. + // 该字段显式写出小数点,故小数部分以点开头。构造数值前先去掉它,否则 + // 解析会遇到两个小数点。 let digits = fraction_digits.strip_prefix('.').unwrap_or(fraction_digits); format!("0.{digits}") .parse::() @@ -297,6 +384,8 @@ fn epoch_field(line: &str, first: usize, last: usize, line_number: usize) -> Res } /// Converts a year and a day of year, both one based, to a calendar date. +/// +/// 把从一开始的年与年日转换为日历日期。 fn calendar_from_day_of_year(year: i32, day_of_year: u32) -> Result<(i32, u32, u32)> { let mut remaining = day_of_year; let mut month = 1; @@ -329,6 +418,15 @@ impl TwoLineElement { /// /// Returns an error describing the first failure and the line on which /// it occurred. + /// + /// 从一行、两行或三行文本解析两线根数集。 + /// + /// 行结束符可以是 `\n`、`\r\n` 或二者混用;空行与全为空白字符的行会被 + /// 忽略。不以 `1 ` 开头的首行被取作卫星名称,这正是可选三行形式的写法。 + /// + /// 每一行都检查第一列的行号、长度与校验和。两行的根数集编号必须一致。 + /// + /// 返回描述首个失败及其所在行的错误。 pub fn parse(text: &str) -> Result { let lines: Vec<&str> = text .lines() @@ -346,6 +444,7 @@ impl TwoLineElement { // The name line, when present, is not part of the checksummed // format. + // 名称行若存在,不属于带校验和的格式。 let first_number = if name.is_some() { 2 } else { 1 }; let second_number = first_number + 1; diff --git a/crates/orbit/apsis-tle/src/field.rs b/crates/orbit/apsis-tle/src/field.rs index a2cbeb1..45efb91 100644 --- a/crates/orbit/apsis-tle/src/field.rs +++ b/crates/orbit/apsis-tle/src/field.rs @@ -3,18 +3,30 @@ use core::fmt; /// The category of a parse failure. +/// +/// 解析失败的类别。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum ElementErrorKind { /// A line is shorter than the format requires. + /// + /// 某行短于格式要求的长度。 TooShort, /// A fixed width field does not contain a well formed value. + /// + /// 某定宽字段不含良构的数值。 InvalidField, /// The checksum at the end of a line does not match the line contents. + /// + /// 行末校验和与该行内容不符。 Checksum, /// The two lines carry different element set numbers. + /// + /// 两行携带不同的根数集编号。 MismatchedElementSet, /// A character where a digit or a specific symbol was expected. + /// + /// 期望数字或特定符号处出现了其他字符。 UnexpectedCharacter, } @@ -37,6 +49,11 @@ impl fmt::Display for ElementErrorKind { /// were being read, so that a caller can point at the offending text. Line /// numbers are one based and column ranges are the one based inclusive /// range from the format definition. +/// +/// 解析两线根数集时的失败。 +/// +/// 该错误记录哪一行失败,并在适用时记录当时正在读取的列,使调用方能指出有问题的 +/// 文本。行号从一开始,列区间为格式定义中从一开始的含两端区间。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ElementError { kind: ElementErrorKind, @@ -51,6 +68,11 @@ impl ElementError { /// The line number is one based. The column range is the one based /// inclusive range that was being read, if the failure is tied to a /// field. + /// + /// 用给定类别、行号与列区间创建错误。 + /// + /// 行号从一开始。列区间为当时正在读取的从一开始的含两端区间,当失败与某个 + /// 字段相关时给出。 pub fn new(kind: ElementErrorKind, line: usize, columns: Option<(usize, usize)>) -> Self { Self { kind, @@ -61,27 +83,37 @@ impl ElementError { } /// Attaches a description of the offending text. + /// + /// 附加对有问题的文本的说明。 pub fn with_detail(mut self, detail: impl Into) -> Self { self.detail = Some(detail.into()); self } /// Returns the category of this failure. + /// + /// 返回该失败的类别。 pub fn kind(&self) -> ElementErrorKind { self.kind } /// Returns the one based number of the line that failed. + /// + /// 返回失败行的从一而行号。 pub fn line(&self) -> usize { self.line } /// Returns the one based inclusive column range that failed, if any. + /// + /// 返回失败的从一开始的含两端列区间,若有。 pub fn columns(&self) -> Option<(usize, usize)> { self.columns } /// Returns the detail text attached to this error, if any. + /// + /// 返回附加在该错误上的说明文本,若有。 pub fn detail(&self) -> Option<&str> { self.detail.as_deref() } @@ -103,4 +135,6 @@ impl fmt::Display for ElementError { impl core::error::Error for ElementError {} /// A [`Result`](core::result::Result) whose error type is [`ElementError`]. +/// +/// 错误类型为 [`ElementError`] 的 [`Result`](core::result::Result)。 pub type Result = core::result::Result; diff --git a/scripts/__pycache__/check-bilingual.cpython-314.pyc b/scripts/__pycache__/check-bilingual.cpython-314.pyc new file mode 100644 index 0000000..c7aa947 Binary files /dev/null and b/scripts/__pycache__/check-bilingual.cpython-314.pyc differ diff --git a/scripts/check-bilingual.py b/scripts/check-bilingual.py index 09cd51e..76bda46 100755 --- a/scripts/check-bilingual.py +++ b/scripts/check-bilingual.py @@ -41,6 +41,32 @@ MOJIBAKE = "�" # `SGP4` in English, so a lowercase word between CJK is a mistake. MIXED = re.compile(r"[\u4e00-\u9fff][a-z]{3,}[\u4e00-\u9fff]") +# A comment that is only a formula or a bare symbol carries no prose to +# translate, so demanding a second language on it would be noise. Such a +# comment is left as it stands, which is the whole of it. +FORMULA = re.compile("^`?[A-Za-z0-9_^*+\\-/() =,;.\u3002\uff0c]*`?[\.\u3002]?$") + +# Three letters in a row is a word, not a symbol, so a line carrying one is +# prose and needs translating even if it is mostly numbers. +PROSE = re.compile(r"[A-Za-z]{3}") + + +def is_formula_only(block: list[str]) -> bool: + """True when every line of the block is code, a formula, or blank.""" + saw_content = False + for line in block: + body = re.sub(r"^\s*(///|//[^/!])\s?", "", line).strip() + if not body: + continue + saw_content = True + if not FORMULA.match(body): + return False + # A single backticked token is a symbol, whatever it spells. Prose + # needs a verb, and `cos` or `eta` has none. + if len(re.findall(r"[A-Za-z]{2,}", body)) > 1: + return False + return saw_content + def blocks(path: Path) -> list[tuple[int, list[str]]]: """Returns the comment blocks of a file with the line each starts on.""" @@ -92,6 +118,9 @@ def main(argv: list[str]) -> int: mixed.append( f"{path.relative_to(ROOT)}:{line} {found.group(0)!r}" ) + if is_formula_only(block): + # Nothing to translate; the symbol is the whole comment. + continue total += 1 if CJK.search(text): translated += 1