diff --git a/content/concepts/foundation/numerical-methods.md b/content/concepts/foundation/numerical-methods.md index 49a1dab..872bd0e 100644 --- a/content/concepts/foundation/numerical-methods.md +++ b/content/concepts/foundation/numerical-methods.md @@ -32,15 +32,15 @@ problem and cannot be estimated from the arithmetic; it has to be recognised. ### Iteration -A fixed point is reached by repeated substitution, `x` becoming `g(x)`. It -converges when the derivative of `g` at the root is small in magnitude: +A fixed point is reached by repeated substitution, $x$ becoming `g(x)`. It +converges when the derivative of $g$ at the root is small in magnitude: $$ \lvert g'(x) \rvert < 1 \quad \text{(convergence)}, \qquad \lvert g'(x) \rvert > 1 \quad \text{(divergence)} $$ -Kepler's equation, `M = E - e sin E`, has no closed form solution for `E`. It +Kepler's equation, $M = E - e\sin E$, has no closed form solution for $E$. It is solved by Newton's method, $$ @@ -58,7 +58,7 @@ whether divergence or cost dominates. ### Conditioning The sensitivity of an answer to a perturbation of its input is the -**condition number**. For a function `f`, +**condition number**. For a function $f$, $$ \kappa = \frac{\lvert f'(x) \rvert \, \lvert x \rvert}{\lvert f(x) \rvert} @@ -68,7 +68,7 @@ A large condition number means a small input error becomes a large output error, whatever the method. Near a singular point the condition number diverges: computing the radius of an orbit from its semi-major axis and eccentricity is well conditioned, while computing the time to perigee from the -same elements is not, because `sqrt(1 - e^2)` appears in the denominator and +same elements is not, because $\sqrt{1 - e^2}$ appears in the denominator and goes to zero as `e` approaches one. The problem is ill conditioned, not the arithmetic, and more digits do not help. @@ -79,8 +79,8 @@ the step, and a local error proportional to the step squared. Halving the step therefore improves the answer by a factor of two, not four, which is why "just halve the step" is an expensive way to buy accuracy. -For a method of order `p`, the global error falls as the step to the power -`p`. A fourth order Runge-Kutta gives `h^4`, which is why it is common: the +For a method of order $p$, the global error falls as the step to the power +$p$. A fourth order Runge-Kutta gives $h^4$, which is why it is common: the cost per step is four function evaluations, and halving the step costs eight times as much for a factor of sixteen less error. diff --git a/content/concepts/foundation/numerical-methods.zh.md b/content/concepts/foundation/numerical-methods.zh.md index 503faae..9bbf4c0 100644 --- a/content/concepts/foundation/numerical-methods.zh.md +++ b/content/concepts/foundation/numerical-methods.zh.md @@ -22,14 +22,14 @@ summary: 当精确答案不存在时,一个算出来的数字会有什么行 ### 迭代 -不动点通过反复代入达到,`x` 变为 `g(x)`。当根附近导数绝对值小于一时收敛: +不动点通过反复代入达到,$x$ 变为 `g(x)`。当根附近导数绝对值小于一时收敛: $$ \lvert g'(x) \rvert < 1 \quad \text{(收敛)}, \qquad \lvert g'(x) \rvert > 1 \quad \text{(发散)} $$ -开普勒方程 `M = E - e sin E` 对 `E` 没有闭式解,用牛顿法求解: +开普勒方程 $M = E - e\sin E$ 对 $E$ 没有闭式解,用牛顿法求解: $$ E_{n+1} = E_n - \frac{E_n - e \sin E_n - M}{1 - e \cos E_n} @@ -41,19 +41,19 @@ $$ ### 条件数 -答案对输入扰动的敏感程度是**条件数**。对函数 `f`, +答案对输入扰动的敏感程度是**条件数**。对函数 $f$, $$ \kappa = \frac{\lvert f'(x) \rvert \, \lvert x \rvert}{\lvert f(x) \rvert} $$ -条件数大意味着小的输入误差变成大的输出误差,与方法无关。在奇点附近条件数发散:由半长轴与偏心率计算轨道半径是良态的,而由同样元素计算近地点时刻不是,因为分母中出现 `sqrt(1 - e^2)`,当 `e` 趋近一时趋于零。问题病态而非算术病态,多给几位有效数字无济于事。 +条件数大意味着小的输入误差变成大的输出误差,与方法无关。在奇点附近条件数发散:由半长轴与偏心率计算轨道半径是良态的,而由同样元素计算近地点时刻不是,因为分母中出现 $\sqrt{1 - e^2}$,当 `e` 趋近一时趋于零。问题病态而非算术病态,多给几位有效数字无济于事。 ### 步长带来的截断 定步长积分的全局误差与步长成正比,局部误差与步长平方成正比。因此步长减半只让答案改善两倍而非四倍,"干脆把步长减半"因此是换精度的昂贵做法。 -对 `p` 阶方法,全局误差按步长的 `p` 次幂下降。四阶 Runge-Kutta 给出 `h^4`,因此常用:每步代价是四次函数求值,步长减半代价增至八倍,而误差减少十六倍。 +对 $p$ 阶方法,全局误差按步长的 $p$ 次幂下降。四阶 Runge-Kutta 给出 $h^4$,因此常用:每步代价是四次函数求值,步长减半代价增至八倍,而误差减少十六倍。 ## 单位约定 diff --git a/content/concepts/foundation/units-and-dimensions.md b/content/concepts/foundation/units-and-dimensions.md index 5d78993..ac8bb91 100644 --- a/content/concepts/foundation/units-and-dimensions.md +++ b/content/concepts/foundation/units-and-dimensions.md @@ -55,14 +55,14 @@ one in a compiler that tracked those types. ### Scaling laws Dimension reasoning earns its keep in scaling arguments, where an error shows -up as an impossible exponent. The gravitational perturbation at distance `r` -from a body of mass `M` must scale as +up as an impossible exponent. The gravitational perturbation at distance $r$ +from a body of mass $M$ must scale as $$ a \propto \frac{G M}{r^2} $$ -because `G` has units of `m^3 kg^-1 s^-2`; any other combination either does +because $G$ has units of $\text{m}^3\,\text{kg}^{-1}\,\text{s}^{-2}$; any other combination either does not have the dimensions of an acceleration or does have them only for a particular case, which is the signal that the argument is incomplete. @@ -71,13 +71,13 @@ particular case, which is the signal that the argument is incomplete. - **Angles.** An angle is dimensionless in principle, because a full turn is one. It is written in radians in most formulae, and in degrees, arcminutes and arcseconds where it is read by a person. The relationship is - `180 / pi` degrees per radian. A rotation described in axis and angle uses + $180 / \pi$ degrees per radian. A rotation described in axis and angle uses the angle in a plane and the axis through the pole; the two conventions differ, which is why a rotation is usually written explicitly as right-hand or left-hand. -- **Prefixes.** SI prefixes are powers of ten: kilo is `10^3`, mega `10^6`, - micro `10^-6`. A kilometre is a thousand metres, never a thousand miles. -- **Binary prefixes.** A mebibyte is `2^20` bytes, not `10^6`. The distinction +- **Prefixes.** SI prefixes are powers of ten: kilo is $10^3$, mega $10^6$, + micro $10^{-6}$. A kilometre is a thousand metres, never a thousand miles. +- **Binary prefixes.** A mebibyte is $2^{20}$ bytes, not $10^6$. The distinction matters wherever a size is compared against a capacity. - **Units in the field.** Orbital work is written in kilometres and seconds, not metres, because the quantities are large and the models are fitted to diff --git a/content/concepts/foundation/units-and-dimensions.zh.md b/content/concepts/foundation/units-and-dimensions.zh.md index 2d78901..ec25155 100644 --- a/content/concepts/foundation/units-and-dimensions.zh.md +++ b/content/concepts/foundation/units-and-dimensions.zh.md @@ -40,19 +40,19 @@ $$ ### 标度关系 -量纲推理在标度分析中格外有用,因为一旦出错就会表现为不可能的指数。距质量为 `M` 的天体 `r` 处的引力摄动必然标度为 +量纲推理在标度分析中格外有用,因为一旦出错就会表现为不可能的指数。距质量为 $M$ 的天体 $r$ 处的引力摄动必然标度为 $$ a \propto \frac{G M}{r^2} $$ -因为 `G` 的单位是 `m^3 kg^-1 s^-2`;任何其他组合要么不具备加速度的量纲,要么只在特定情形成立,后者正是论证不完整的信号。 +因为 $G$ 的单位是 $\text{m}^3\,\text{kg}^{-1}\,\text{s}^{-2}$;任何其他组合要么不具备加速度的量纲,要么只在特定情形成立,后者正是论证不完整的信号。 ## 单位约定 -- **角度**。角度在原理上是无量纲的,因为一整圈就是 1。多数公式中以弧度书写,而被人读取时用度、角分、角秒。两者关系是每弧度 `180 / pi` 度。用轴角描述旋转时,角在平面内、轴穿过极点;两种约定并不相同,因此旋转通常显式写出右手或左手。 -- **词头**。SI 词头是十的幂:千为 `10^3`,兆为 `10^6`,微为 `10^-6`。一千米是一千公尺,绝不是一千英里。 -- **二进制词头**。一兆二进制字节是 `2^20` 字节,不是 `10^6`。凡是把容量与大小作比较之处,这一区别都重要。 +- **角度**。角度在原理上是无量纲的,因为一整圈就是 1。多数公式中以弧度书写,而被人读取时用度、角分、角秒。两者关系是每弧度 $180 / \pi$ 度。用轴角描述旋转时,角在平面内、轴穿过极点;两种约定并不相同,因此旋转通常显式写出右手或左手。 +- **词头**。SI 词头是十的幂:千为 $10^3$,兆为 $10^6$,微为 $10^{-6}$。一千米是一千公尺,绝不是一千英里。 +- **二进制词头**。一兆二进制字节是 $2^{20}$ 字节,不是 $10^6$。凡是把容量与大小作比较之处,这一区别都重要。 - **领域内的单位**。轨道工作用千米与秒书写,而不是米,因为量级大且模型是按这些单位拟合的。用米表达的传播模型与用千米表达的编目数据会相差一千倍,而且不会报错。 ## 适用边界 diff --git a/content/concepts/implementation/standards-and-provenance.md b/content/concepts/implementation/standards-and-provenance.md index aa1c167..402a037 100644 --- a/content/concepts/implementation/standards-and-provenance.md +++ b/content/concepts/implementation/standards-and-provenance.md @@ -37,7 +37,7 @@ Naming the source makes these choices checkable. | Earth Rotation Angle | *IERS Conventions (2010)*, equation 8.32 | `apsis-frames` | | Geodetic conversions | NIMA TR8350.2, *Department of Defense World Geodetic System 1984*, third edition, 2000 | `apsis-geodesy` | | Julian and Modified Julian Date | IAU conventions as described in the *Explanatory Supplement to the Astronomical Almanac* | `apsis-time` | -| Terrestrial Time offset | IAU 2000 Resolution B1.9, defining `TT - TAI = 32.184 s` | `apsis-time` | +| Terrestrial Time offset | IAU 2000 Resolution B1.9, defining $\text{TT} - \text{TAI} = 32.184\,\text{s}$ | `apsis-time` | | Leap seconds | IERS Bulletin C, embedded as a table by `apsis-leap` | `apsis-leap` | | Calendar algorithms | Howard Hinnant, *chrono-Compatible Low-Level Date Algorithms*, 2015 | `apsis-time` | | Discrete event simulation | Zeigler, Praehofer, Kim, *Theory of Modeling and Simulation*, second edition, 2000 | `apsis-sim-engine` | diff --git a/content/concepts/implementation/standards-and-provenance.zh.md b/content/concepts/implementation/standards-and-provenance.zh.md index 5e1b320..6bf834a 100644 --- a/content/concepts/implementation/standards-and-provenance.zh.md +++ b/content/concepts/implementation/standards-and-provenance.zh.md @@ -28,7 +28,7 @@ summary: 每个已实现算法各自遵循哪一公开标准或论文,以及 | 地球自转角 | *IERS Conventions (2010)*,公式 8.32 | `apsis-frames` | | 大地坐标转换 | NIMA TR8350.2,*Department of Defense World Geodetic System 1984*,第三版,2000 | `apsis-geodesy` | | 儒略日与约化儒略日 | IAU 约定,见《天文年历解释性补充》 | `apsis-time` | -| 地球时偏差 | IAU 2000 决议 B1.9,定义 `TT - TAI = 32.184 s` | `apsis-time` | +| 地球时偏差 | IAU 2000 决议 B1.9,定义 $\text{TT} - \text{TAI} = 32.184\,\text{s}$ | `apsis-time` | | 闰秒 | IERS Bulletin C,由 `apsis-leap` 以表形式内嵌 | `apsis-leap` | | 日历算法 | Howard Hinnant,*chrono-Compatible Low-Level Date Algorithms*,2015 | `apsis-time` | | 离散事件仿真 | Zeigler、Praehofer、Kim,*Theory of Modeling and Simulation*,第二版,2000 | `apsis-sim-engine` | diff --git a/content/concepts/network/access-and-availability.md b/content/concepts/network/access-and-availability.md index d45a92e..2f01d85 100644 --- a/content/concepts/network/access-and-availability.md +++ b/content/concepts/network/access-and-availability.md @@ -26,10 +26,10 @@ across the line of sight. ### The geometric pass -A satellite in a circular orbit at semi-major axis `a` passes a latitude -`phi` with a maximum elevation `e_max` that follows from the geometry, +A satellite in a circular orbit at semi-major axis $a$ passes a latitude +$\phi$ with a maximum elevation $e_{\text{max}}$ that follows from the geometry, -`e_max` follows from the geometry: it is the angle whose tangent is the +$e_{\text{max}}$ follows from the geometry: it is the angle whose tangent is the perpendicular distance from the observer to the orbit plane divided by the distance along the line of sight. diff --git a/content/concepts/network/access-and-availability.zh.md b/content/concepts/network/access-and-availability.zh.md index 6825c07..58da5ce 100644 --- a/content/concepts/network/access-and-availability.zh.md +++ b/content/concepts/network/access-and-availability.zh.md @@ -18,7 +18,7 @@ summary: 卫星在头顶与用户能用之间的区别,以及两者之间的 ### 几何过境 -半长轴为 `a` 的圆轨道卫星经过纬度 `phi` 时,最大仰角 `e_max` 由几何决定:观测者到轨道面的垂距,与视线方向距离之比的反正切。 +半长轴为 $a$ 的圆轨道卫星经过纬度 $\phi$ 时,最大仰角 $e_{\text{max}}$ 由几何决定:观测者到轨道面的垂距,与视线方向距离之比的反正切。 有两个极限值得点名。在极点,倾斜轨道上的卫星最好只能达到 `90 - inclination` 度的仰角,因此高倾角星座在高纬度依赖众多卫星而非仰角高的单星。在赤道,赤道轨道卫星直接从头顶过境,仰角接近 90 度,这是系统中最好的情形。 diff --git a/content/concepts/network/constellations.md b/content/concepts/network/constellations.md index fda02c8..d130693 100644 --- a/content/concepts/network/constellations.md +++ b/content/concepts/network/constellations.md @@ -29,14 +29,14 @@ poorly covered at any given place and time. ### The geometry of a shell -At altitude `h`, the Earth subtends a half angle from the satellite, and the +At altitude $h$, the Earth subtends a half angle from the satellite, and the ground area visible above an elevation angle `e` is $$ A(e) = 2 \pi R^2 \left( 1 + \cos e \right) $$ -For `e = 0`, the visible cap is a hemisphere. The fraction of the globe a +For $e = 0$, the visible cap is a hemisphere. The fraction of the globe a satellite can see at once is therefore fixed by its altitude alone, and no amount of satellites improves it beyond covering more of that hemisphere at the same time. diff --git a/content/concepts/network/constellations.zh.md b/content/concepts/network/constellations.zh.md index e936a70..86ce1c7 100644 --- a/content/concepts/network/constellations.zh.md +++ b/content/concepts/network/constellations.zh.md @@ -23,13 +23,13 @@ summary: 星座为何是一个关于轨道面、相位与壳层的算术问题 ### 壳层的几何 -在高度 `h` 处地球对卫星张成半角,仰角 `e` 以上的可见地面面积为 +在高度 $h$ 处地球对卫星张成半角,仰角 `e` 以上的可见地面面积为 $$ A(e) = 2 \pi R^2 \left( 1 + \cos e \right) $$ -`e = 0` 时可见的是一个半球。因此单颗卫星同时可见的全球比例只由其高度决定,再多的卫星也不能改善它覆盖该半球的程度。 +$e = 0$ 时可见的是一个半球。因此单颗卫星同时可见的全球比例只由其高度决定,再多的卫星也不能改善它覆盖该半球的程度。 ### 单位覆盖的成本 diff --git a/content/concepts/orbit/atmospheric-drag.md b/content/concepts/orbit/atmospheric-drag.md index ffd0a32..5ac6932 100644 --- a/content/concepts/orbit/atmospheric-drag.md +++ b/content/concepts/orbit/atmospheric-drag.md @@ -18,15 +18,15 @@ $$ F = -\tfrac{1}{2} \rho v^2 C_d A $$ -with `rho` the atmospheric density, `v` the speed, `Cd` a drag coefficient of -order one, and `A` the projected area. What matters to the orbit is the +with $\rho$ the atmospheric density, $v$ the speed, `Cd` a drag coefficient of +order one, and $A$ the projected area. What matters to the orbit is the combination $$ B = \frac{C_d A}{m} $$ -the inverse of the **ballistic coefficient**, and `B` is what a drag model +the inverse of the **ballistic coefficient**, and $B$ is what a drag model actually needs. Above about 700 km the density is small enough that the force is comparable @@ -52,7 +52,7 @@ $$ H = \frac{k T}{m g} $$ -with `T` the temperature, which itself depends on the solar activity. A warmer +with $T$ the temperature, which itself depends on the solar activity. A warmer thermosphere means a larger scale height, a higher density at every altitude, and faster decay. @@ -67,7 +67,7 @@ over one solar cycle does not transfer to another. ### Effect on the orbit The effect on the semi-major axis is a secular decay, with a rate that scales -with `rho / m` and with the inverse of the semi-major axis cubed, because the +with $\rho / m$ and with the inverse of the semi-major axis cubed, because the orbit speed falls with altitude. The practical consequence is that the decay rate is not constant over the life of the orbit; a satellite that starts in a dense layer decays quickly and then slows as it sinks into a thinner one. @@ -113,6 +113,6 @@ which is why such derived values are more reliable than a propagated one. ## See also -- [[perturbations]] for the ordering of drag against `J_2` and third body +- [[perturbations]] for the ordering of drag against $J_2$ and third body - [[ephemeris-interpolation]] for a track that stops needing a density model - [[sgp4]] for a propagation that carries a fitted drag term \ No newline at end of file diff --git a/content/concepts/orbit/atmospheric-drag.zh.md b/content/concepts/orbit/atmospheric-drag.zh.md index d74c042..883b0db 100644 --- a/content/concepts/orbit/atmospheric-drag.zh.md +++ b/content/concepts/orbit/atmospheric-drag.zh.md @@ -16,13 +16,13 @@ $$ F = -\tfrac{1}{2} \rho v^2 C_d A $$ -其中 `rho` 为大气密度、`v` 为速度、`Cd` 为量级在一左右的阻力系数、`A` 为投影面积。对轨道真正重要的是其组合 +其中 $\rho$ 为大气密度、$v$ 为速度、`Cd` 为量级在一左右的阻力系数、$A$ 为投影面积。对轨道真正重要的是其组合 $$ B = \frac{C_d A}{m} $$ -即**弹道系数**的倒数,而 `B` 正是阻力模型实际需要的量。 +即**弹道系数**的倒数,而 $B$ 正是阻力模型实际需要的量。 在约 700 km 以上,密度小到该力可与其他摄动相比,而它在过渡区的整个高度范围内变化若干个数量级,这是一多半困难的来源。 @@ -40,7 +40,7 @@ $$ H = \frac{k T}{m g} $$ -其中 `T` 为温度,而温度本身又取决于太阳活动。更热的热层意味着更大的标高、每个高度上更高的密度,以及更快的衰减。 +其中 $T$ 为温度,而温度本身又取决于太阳活动。更热的热层意味着更大的标高、每个高度上更高的密度,以及更快的衰减。 ### 太阳活动 @@ -48,7 +48,7 @@ $$ ### 对轨道的影响 -对半长轴的影响是长期衰减,其速率正比于 `rho / m`,并正比于半长轴三次方之逆,因为轨道速度随高度下降。实际后果是衰减率在轨道寿命内并不恒定:起始于稠密层的卫星衰减很快,进入稀薄层后减缓。 +对半长轴的影响是长期衰减,其速率正比于 $\rho / m$,并正比于半长轴三次方之逆,因为轨道速度随高度下降。实际后果是衰减率在轨道寿命内并不恒定:起始于稠密层的卫星衰减很快,进入稀薄层后减缓。 ### 解析模型 @@ -76,6 +76,6 @@ $$ ## 另见 -- [[perturbations]]:阻力相对 `J_2` 与三体引力的大小次序 +- [[perturbations]]:阻力相对 $J_2$ 与三体引力的大小次序 - [[ephemeris-interpolation]]:不再需要密度模型的轨迹 - [[sgp4]]:携带拟合阻力项的传播 \ No newline at end of file diff --git a/content/concepts/orbit/ephemeris-interpolation.md b/content/concepts/orbit/ephemeris-interpolation.md index 73b1908..e49602c 100644 --- a/content/concepts/orbit/ephemeris-interpolation.md +++ b/content/concepts/orbit/ephemeris-interpolation.md @@ -29,7 +29,7 @@ $$ p(s) = h_{00}(s) \, p_0 + h_{10}(s) \, \Delta t \, v_0 + h_{01}(s) \, p_1 + h_{11}(s) \, \Delta t \, v_1 $$ -where `s` is the fraction along the interval in `[0, 1]`, `dt` is the +where $s$ is the fraction along the interval in `[0, 1]`, `dt` is the interval length, and $$ @@ -44,7 +44,7 @@ $$ are the Hermite basis polynomials. The velocity is the derivative of the same polynomial with respect to time. -Because the velocity is used at both ends, the interpolation is `C^1`: the +Because the velocity is used at both ends, the interpolation is $C^1$: the position and the velocity are continuous across a sample boundary. A table built from position alone would have a discontinuous velocity at every sample, which is visible in a plot as a kink and is wrong for anything that diff --git a/content/concepts/orbit/ephemeris-interpolation.zh.md b/content/concepts/orbit/ephemeris-interpolation.zh.md index 3e95c2f..3060e83 100644 --- a/content/concepts/orbit/ephemeris-interpolation.zh.md +++ b/content/concepts/orbit/ephemeris-interpolation.zh.md @@ -22,7 +22,7 @@ $$ p(s) = h_{00}(s) \, p_0 + h_{10}(s) \, \Delta t \, v_0 + h_{01}(s) \, p_1 + h_{11}(s) \, \Delta t \, v_1 $$ -其中 `s` 是区间内 `[0, 1]` 的比例,`dt` 是区间长度,而 +其中 $s$ 是区间内 `[0, 1]` 的比例,`dt` 是区间长度,而 $$ \begin{aligned} @@ -35,7 +35,7 @@ $$ 是 Hermite 基多项式。速度是同一多项式对时间的导数。 -由于两端都用到速度,插值是 `C^1` 的:位置与速度在采样边界处连续。仅由位置构建的表在每个采样点速度不连续,在图上表现为折角,对任何需要求导的用途都是错误的。 +由于两端都用到速度,插值是 $C^1$ 的:位置与速度在采样边界处连续。仅由位置构建的表在每个采样点速度不连续,在图上表现为折角,对任何需要求导的用途都是错误的。 单个区间上的插值误差随区间长度的四次方乘以位置的四阶导数增长。正是该量决定采样间距。 diff --git a/content/concepts/orbit/higher-order-gravity.md b/content/concepts/orbit/higher-order-gravity.md index 09080da..210fe02 100644 --- a/content/concepts/orbit/higher-order-gravity.md +++ b/content/concepts/orbit/higher-order-gravity.md @@ -23,38 +23,38 @@ U(r, \theta, \lambda) = \frac{\mu}{r} \left[ 1 \left( C_{nm} \cos m\lambda + S_{nm} \sin m\lambda \right) \right] $$ -- `r` is the distance from the centre, `theta` the geocentric colatitude and - `lambda` the longitude. -- `P_nm` are the Legendre functions, real polynomials on `sin(theta)`. -- `C_nm` and `S_nm` are the **geopotential coefficients**; the normalised ones +- $r$ is the distance from the centre, $\theta$ the geocentric colatitude and + $\lambda$ the longitude. +- $P_{nm}$ are the Legendre functions, real polynomials on $\sin\theta$. +- $C_{nm}$ and $S_{nm}$ are the **geopotential coefficients**; the normalised ones are what a model publishes. -- `R` is a reference radius. +- $R$ is a reference radius. -For `n = 0` there is one coefficient, the gravitational parameter. For each +For $n = 0$ there is one coefficient, the gravitational parameter. For each higher degree there are `2n + 1` zonal, tesseral and sectorial terms. -`J_2` is the second zonal coefficient, about `1.0826e-3`, and it dominates -everything above it. The next largest are `J_3`, `J_4`, each smaller than +$J_2$ is the second zonal coefficient, about `1.0826e-3`, and it dominates +everything above it. The next largest are $J_3$, $J_4$, each smaller than the last. ## Mathematics ### What each term does -- **Zonal** coefficients (`m = 0`) are axisymmetric and cause secular drift +- **Zonal** coefficients ($m = 0$) are axisymmetric and cause secular drift in the node and the argument of perigee, and no change in inclination. This is the classical J2 problem. -- **Tesseral** coefficients (`0 < m < n`) are longitude dependent and cause +- **Tesseral** coefficients ($0 < m < n$) are longitude dependent and cause short-period variation in every element including the inclination. This is why real orbits wobble rather than precessing smoothly. -- **Sectorial** coefficients (`m = n`) couple the radius and the longitude +- **Sectorial** coefficients ($m = n$) couple the radius and the longitude directly and are the smallest contribution. ### The truncation problem -The series is a power series in `(R/r)`, which converges only for `r > R`, -that is, outside the Earth. For a satellite at `r` above the surface the -ratio is `R/r < 1` and the series converges, but **slowly**, because `R/r` is +The series is a power series in $(R/r)$, which converges only for $r > R$, +that is, outside the Earth. For a satellite at $r$ above the surface the +ratio is $R/r < 1$ and the series converges, but **slowly**, because $R/r$ is close to one at low altitude. The number of terms needed for a given accuracy grows as the altitude falls, which is why a low Earth orbit model truncates hard and a geostationary one can carry many terms. @@ -73,14 +73,14 @@ $$ \begin{bmatrix} \bar{C}_{nm} \\ \bar{S}_{nm} \end{bmatrix} $$ -A model is defined by its degree `N` and by the coefficients below it. Degree +A model is defined by its degree $N$ and by the coefficients below it. Degree 180 with the first few hundred coefficients is typical. ## Unit conventions -- Potential and coefficients dimensionless; `mu` is the gravitational +- Potential and coefficients dimensionless; $\mu$ is the gravitational parameter. -- `R` is the equatorial radius, about 6378.137 km. +- $R$ is the equatorial radius, about 6378.137 km. - Angle in radians; the Legendre functions are polynomials on the sine of the colatitude. @@ -88,7 +88,7 @@ A model is defined by its degree `N` and by the coefficients below it. Degree A truncated gravity field is a compromise. Cutting at a low degree saves computation and costs accuracy, and the cost is largest at low altitude where -`(R/r)` approaches one. The numerical consequences are worse than the +$(R/r)$ approaches one. The numerical consequences are worse than the positional ones: a truncated field is smooth, but its derivatives, which drive the short-period terms, are not. diff --git a/content/concepts/orbit/higher-order-gravity.zh.md b/content/concepts/orbit/higher-order-gravity.zh.md index 42918c3..16de879 100644 --- a/content/concepts/orbit/higher-order-gravity.zh.md +++ b/content/concepts/orbit/higher-order-gravity.zh.md @@ -18,26 +18,26 @@ U(r, \theta, \lambda) = \frac{\mu}{r} \left[ 1 \left( C_{nm} \cos m\lambda + S_{nm} \sin m\lambda \right) \right] $$ -- `r` 是到地心的距离,`theta` 是地心余纬,`lambda` 是经度。 -- `P_nm` 是勒让德函数,是 `sin(theta)` 上的实多项式。 -- `C_nm` 与 `S_nm` 是**地球位势系数**;模型公布的是归一化后的系数。 -- `R` 是参考半径。 +- $r$ 是到地心的距离,$\theta$ 是地心余纬,$\lambda$ 是经度。 +- $P_{nm}$ 是勒让德函数,是 $\sin\theta$ 上的实多项式。 +- $C_{nm}$ 与 $S_{nm}$ 是**地球位势系数**;模型公布的是归一化后的系数。 +- $R$ 是参考半径。 -`n = 0` 只有一个系数,即引力参数。每高一个阶次就有 `2n + 1` 个带谐、田谐与扇谐项。 +$n = 0$ 只有一个系数,即引力参数。每高一个阶次就有 `2n + 1` 个带谐、田谐与扇谐项。 -`J_2` 是二阶带谐系数,约为 `1.0826e-3`,并在它之上的一切中占主导。其后依次是 `J_3`、`J_4`,逐次减小。 +$J_2$ 是二阶带谐系数,约为 `1.0826e-3`,并在它之上的一切中占主导。其后依次是 $J_3$、$J_4$,逐次减小。 ## 数学含义 ### 每一项的作用 -- **带谐**系数(`m = 0`)轴对称,造成升交点与近地点角的长期漂移,而不改变倾角。这就是经典的 J2 问题。 -- **田谐**系数(`0 < m < n`)依赖经度,造成所有根数(含倾角)的短周期变化。这就是真实轨道是摇摆而非平滑进动的原因。 -- **扇谐**系数(`m = n`)直接耦合半径与经度,贡献最小。 +- **带谐**系数($m = 0$)轴对称,造成升交点与近地点角的长期漂移,而不改变倾角。这就是经典的 J2 问题。 +- **田谐**系数($0 < m < n$)依赖经度,造成所有根数(含倾角)的短周期变化。这就是真实轨道是摇摆而非平滑进动的原因。 +- **扇谐**系数($m = n$)直接耦合半径与经度,贡献最小。 ### 截断问题 -该级数是 `(R/r)` 的幂级数,只在 `r > R` 即地球外部收敛。对地表以上的卫星,比值 `R/r < 1`,级数确实收敛,但**很慢**,因为低轨处 `R/r` 趋近一。为达到给定精度所需的项数随高度降低而增长,这就是低轨模型不得不硬截断、而地球静止轨道模型可以携带很多项的原因。 +该级数是 $(R/r)$ 的幂级数,只在 $r > R$ 即地球外部收敛。对地表以上的卫星,比值 $R/r < 1$,级数确实收敛,但**很慢**,因为低轨处 $R/r$ 趋近一。为达到给定精度所需的项数随高度降低而增长,这就是低轨模型不得不硬截断、而地球静止轨道模型可以携带很多项的原因。 这与直觉相反:模型在引力**最强**处更差,而不是在引力最弱处。 @@ -51,17 +51,17 @@ $$ \begin{bmatrix} \bar{C}_{nm} \\ \bar{S}_{nm} \end{bmatrix} $$ -一个模型由其最高阶 `N` 以及此下的系数定义。180 阶配前几百个系数是常见配置。 +一个模型由其最高阶 $N$ 以及此下的系数定义。180 阶配前几百个系数是常见配置。 ## 单位约定 -- 势与系数为无量纲;`mu` 为引力参数。 -- `R` 为赤道半径,约 6378.137 km。 +- 势与系数为无量纲;$\mu$ 为引力参数。 +- $R$ 为赤道半径,约 6378.137 km。 - 角度为弧度;勒让德函数是正弦上的多项式。 ## 适用边界 -截断的引力场是一种折中。低阶截断省下计算并损失精度,而代价在低轨最大,因为那里 `(R/r)` 趋近一。数值上的后果比位置上的更糟:截断的场是光滑的,但驱动短周期项的它的导数不是。 +截断的引力场是一种折中。低阶截断省下计算并损失精度,而代价在低轨最大,因为那里 $(R/r)$ 趋近一。数值上的后果比位置上的更糟:截断的场是光滑的,但驱动短周期项的它的导数不是。 由力函数而非势函数拟合出的模型是另一种做法,在场由特定轨道上的测量给出时使用;它不能推广到其他高度或倾角。 diff --git a/content/concepts/orbit/keplerian-elements.md b/content/concepts/orbit/keplerian-elements.md index 5e53e04..4f5a2a3 100644 --- a/content/concepts/orbit/keplerian-elements.md +++ b/content/concepts/orbit/keplerian-elements.md @@ -15,11 +15,11 @@ against. This document defines them and the conversions. | Element | Symbol | Meaning | Unit | |---------|--------|---------|------| -| Semi-major axis | `a` | Size of the ellipse | km | +| Semi-major axis | $a$ | Size of the ellipse | km | | Eccentricity | `e` | Shape, `0` for a circle, below `1` for an ellipse | dimensionless | -| Inclination | `i` | Tilt of the orbit plane against the equator | rad | +| Inclination | $i$ | Tilt of the orbit plane against the equator | rad | | Right ascension of the ascending node | `Omega` | Where the orbit crosses the equator going north | rad | -| Argument of perigee | `omega` | Angle from the ascending node to perigee, in the orbit plane | rad | +| Argument of perigee | $\omega$ | Angle from the ascending node to perigee, in the orbit plane | rad | | True anomaly | `nu` | Angle from perigee to the satellite | rad | The first two give the shape and size. The second two give the orientation @@ -39,14 +39,14 @@ $$ r = \frac{p}{1 + e \cos\nu}, \qquad p = a (1 - e^2) $$ -where `p` is the semi-latus rectum. The velocity follows from the vis-viva +where $p$ is the semi-latus rectum. The velocity follows from the vis-viva equation: $$ v^2 = \mu \left( \frac{2}{r} - \frac{1}{a} \right) $$ -where `mu` is the gravitational parameter of the Earth. +where $\mu$ is the gravitational parameter of the Earth. Two derived quantities appear often: @@ -82,8 +82,8 @@ apogee and differ in between. | Anomaly | Symbol | Defined as | Geometric meaning | |---------|--------|------------|-------------------| | True anomaly | `nu` | Angle at the focus from perigee | Actual direction of the satellite | -| Eccentric anomaly | `E` | Angle at the center of the ellipse | Auxiliary angle of the projecting circle | -| Mean anomaly | `M` | `M = E - e sin(E)` | Angle a fictitious body on a circular orbit would have | +| Eccentric anomaly | $E$ | Angle at the center of the ellipse | Auxiliary angle of the projecting circle | +| Mean anomaly | $M$ | $M = E - e\sin E$ | Angle a fictitious body on a circular orbit would have | The relations are @@ -91,8 +91,8 @@ $$ \tan\frac{\nu}{2} = \sqrt{\frac{1 + e}{1 - e}} \tan\frac{E}{2}, \qquad M = E - e \sin E $$ -`M = E - e sin(E)` is Kepler's equation. It is transcendental, so `E` is -recovered from `M` by iteration; the implementation uses Newton's method +$M = E - e\sin E$ is Kepler's equation. It is transcendental, so $E$ is +recovered from $M$ by iteration; the implementation uses Newton's method with a fixed iteration budget and a tolerance, which converges quickly for the eccentricities an Earth orbit has. The equation has no closed form solution, and any implementation that claims one is approximating. @@ -106,7 +106,7 @@ because both conversions above are simple in it. - Angles in the interface are in **radians**. A tool that accepts degrees converts on the input boundary and reports both. - The eccentricity is **dimensionless**. -- The gravitational parameter `mu` is in **cubic kilometers per second +- The gravitational parameter $\mu$ is in **cubic kilometers per second squared**. ## Applicability diff --git a/content/concepts/orbit/keplerian-elements.zh.md b/content/concepts/orbit/keplerian-elements.zh.md index a4310e5..d990471 100644 --- a/content/concepts/orbit/keplerian-elements.zh.md +++ b/content/concepts/orbit/keplerian-elements.zh.md @@ -12,11 +12,11 @@ summary: 六个经典轨道元素、二体解与三种近点角。 | 元素 | 符号 | 含义 | 单位 | |------|------|------|------| -| 半长轴 | `a` | 椭圆的大小 | km | +| 半长轴 | $a$ | 椭圆的大小 | km | | 偏心率 | `e` | 形状,`0` 为圆,小于 `1` 为椭圆 | 无量纲 | -| 倾角 | `i` | 轨道面相对赤道的倾角 | rad | +| 倾角 | $i$ | 轨道面相对赤道的倾角 | rad | | 升交点赤经 | `Omega` | 轨道向北穿越赤道的位置 | rad | -| 近地点幅角 | `omega` | 轨道面内从升交点到近地点的角度 | rad | +| 近地点幅角 | $\omega$ | 轨道面内从升交点到近地点的角度 | rad | | 真近点角 | `nu` | 从近地点到卫星的角度 | rad | 前两个给出形状与大小,接下来两个给出轨道面的朝向,最后两个给出近地点在轨道面内的位置与卫星沿轨道的位置。 @@ -31,13 +31,13 @@ $$ r = \frac{p}{1 + e \cos\nu}, \qquad p = a (1 - e^2) $$ -其中 `p` 是半通径。速度由活力公式给出: +其中 $p$ 是半通径。速度由活力公式给出: $$ v^2 = \mu \left( \frac{2}{r} - \frac{1}{a} \right) $$ -其中 `mu` 是地球的引力参数。 +其中 $\mu$ 是地球的引力参数。 两个导出量经常出现: @@ -69,8 +69,8 @@ $$ | 近点角 | 符号 | 定义 | 几何含义 | |--------|------|------|----------| | 真近点角 | `nu` | 焦点处自近地点量起的角 | 卫星的实际方向 | -| 偏近点角 | `E` | 椭圆中心处的角 | 投影圆的辅助角 | -| 平近点角 | `M` | `M = E - e sin(E)` | 假想的圆轨道天体所具有的角 | +| 偏近点角 | $E$ | 椭圆中心处的角 | 投影圆的辅助角 | +| 平近点角 | $M$ | $M = E - e\sin E$ | 假想的圆轨道天体所具有的角 | 关系为 @@ -78,7 +78,7 @@ $$ \tan\frac{\nu}{2} = \sqrt{\frac{1 + e}{1 - e}} \tan\frac{E}{2}, \qquad M = E - e \sin E $$ -`M = E - e sin(E)` 即开普勒方程。它是超越方程,因此由 `M` 求 `E` 需迭代;实现采用带固定迭代次数与容差的牛顿法,对地球轨道通常具有的偏心率收敛很快。该方程没有闭式解,声称有闭式解的实现都是在近似。 +$M = E - e\sin E$ 即开普勒方程。它是超越方程,因此由 $M$ 求 $E$ 需迭代;实现采用带固定迭代次数与容差的牛顿法,对地球轨道通常具有的偏心率收敛很快。该方程没有闭式解,声称有闭式解的实现都是在近似。 偏近点角是多数计算内部使用的量,因为上述两个转换在它之下都很简单。 @@ -87,7 +87,7 @@ $$ - 半长轴单位为**千米**。 - 接口中角度单位为**弧度**。接受度的工具在输入边界处转换,并同时报告两种单位。 - 偏心率为**无量纲**。 -- 引力参数 `mu` 单位为**立方千米每二次方秒**。 +- 引力参数 $\mu$ 单位为**立方千米每二次方秒**。 ## 适用边界 diff --git a/content/concepts/orbit/maneuvers.md b/content/concepts/orbit/maneuvers.md index 97235b7..07345bc 100644 --- a/content/concepts/orbit/maneuvers.md +++ b/content/concepts/orbit/maneuvers.md @@ -37,7 +37,7 @@ $$ \Delta v = v_e \ln\frac{m_0}{m_f} $$ -with `v_e` the exhaust velocity. Since a logarithmic function grows slowly, +with $v_e$ the exhaust velocity. Since a logarithmic function grows slowly, doubling the propellant gives far less than double the velocity. This is why the delta-v of a mission is the primary constraint on its design, and why staging exists. @@ -49,7 +49,7 @@ Hohmann transfer. The first burn raises or lowers the apoapsis to match the target, the satellite coasts through half a revolution, and the second burn circularises. -With `r_1`, `r_2` the two radii and `a_t = (r_1 + r_2) / 2` the transfer +With $r_1$, $r_2$ the two radii and $a_t = (r_1 + r_2) / 2$ the transfer semi-major axis, the two burns follow from the vis-viva equation, $$ @@ -82,13 +82,13 @@ $$ \Delta v_{\text{plane}} \sim 2 V \sin\frac{\Delta i}{2} $$ -with `V` the circular speed at the radius where the rotation is performed. +with $V$ the circular speed at the radius where the rotation is performed. ### Finite burns Over a finite burn the satellite is still moving against gravity and the thrust does not act along the instantaneous velocity. The achieved velocity -change is less than the ideal `a * t`, and the shortfall is the gravity loss, +change is less than the ideal $a t$, and the shortfall is the gravity loss, $$ L_{\text{gravity}} = \left\lvert \Delta v_{\text{ideal}} \right\rvert - \left\lvert \Delta v_{\text{achieved}} \right\rvert @@ -101,7 +101,7 @@ An electric thruster is not. - Delta-v and velocity in kilometres per second. - Radii in kilometres. -- The gravitational parameter `mu` in cubic kilometres per second squared. +- The gravitational parameter $\mu$ in cubic kilometres per second squared. - Inclinations in radians; degrees hide a factor of about 57 when used in a sine. diff --git a/content/concepts/orbit/maneuvers.zh.md b/content/concepts/orbit/maneuvers.zh.md index e2f8d6b..777fcef 100644 --- a/content/concepts/orbit/maneuvers.zh.md +++ b/content/concepts/orbit/maneuvers.zh.md @@ -26,13 +26,13 @@ $$ \Delta v = v_e \ln\frac{m_0}{m_f} $$ -其中 `v_e` 为喷气速度。由于对数函数增长缓慢,推进剂加倍远不能使速度加倍。这就是任务的速度增量是设计首要约束的原因,也是分级存在的原因。 +其中 $v_e$ 为喷气速度。由于对数函数增长缓慢,推进剂加倍远不能使速度加倍。这就是任务的速度增量是设计首要约束的原因,也是分级存在的原因。 ### 霍曼转移 在两个共面圆轨道之间,最省的两脉冲转移就是霍曼转移。第一次点火把远地点或近地点抬升或降低到与目标一致,卫星滑行半个周期,第二次点火圆化。 -设 `r_1`、`r_2` 为两个半径、`a_t = (r_1 + r_2) / 2` 为转移半长轴,两次点火由活力公式给出: +设 $r_1$、$r_2$ 为两个半径、$a_t = (r_1 + r_2) / 2$ 为转移半长轴,两次点火由活力公式给出: $$ \begin{aligned} @@ -57,11 +57,11 @@ $$ \Delta v_{\text{变轨面}} \sim 2 V \sin\frac{\Delta i}{2} $$ -其中 `V` 是完成转动处半径上的圆轨道速度。 +其中 $V$ 是完成转动处半径上的圆轨道速度。 ### 有限推力 -在有限推力过程中,卫星仍在抵抗重力运动,且推力不沿瞬时速度方向。实际达到的速度增量小于理想的 `a * t`,差值即重力损失: +在有限推力过程中,卫星仍在抵抗重力运动,且推力不沿瞬时速度方向。实际达到的速度增量小于理想的 $a t$,差值即重力损失: $$ L_{\text{重力}} = \left\lvert \Delta v_{\text{理想}} \right\rvert - \left\lvert \Delta v_{\text{实际}} \right\rvert @@ -73,7 +73,7 @@ $$ - 速度增量与速度:千米每秒。 - 半径:千米。 -- 引力参数 `mu`:立方千米每二次方秒。 +- 引力参数 $\mu$:立方千米每二次方秒。 - 倾角:弧度;在正弦中使用度会隐藏约 57 的因子。 ## 适用边界 diff --git a/content/concepts/orbit/perturbations.md b/content/concepts/orbit/perturbations.md index 80b55b7..74ab11b 100644 --- a/content/concepts/orbit/perturbations.md +++ b/content/concepts/orbit/perturbations.md @@ -49,7 +49,7 @@ $$ z \left( 3 - 5 z^2/r^2 \right) \end{bmatrix} $$ -with `R` the equatorial radius and `z` measured along the rotation axis. It +with $R$ the equatorial radius and `z` measured along the rotation axis. It has no component that changes the energy, so the size of the orbit is unaffected to first order; what it changes is the orientation. Its secular effects are diff --git a/content/concepts/orbit/perturbations.zh.md b/content/concepts/orbit/perturbations.zh.md index aa208e9..6faea1c 100644 --- a/content/concepts/orbit/perturbations.zh.md +++ b/content/concepts/orbit/perturbations.zh.md @@ -41,7 +41,7 @@ $$ z \left( 3 - 5 z^2/r^2 \right) \end{bmatrix} $$ -其中 `R` 是赤道半径,`z` 沿自转轴量取。它没有改变能量的分量,因此一阶上不影响轨道大小,改变的是轨道朝向。其长期效应为 +其中 $R$ 是赤道半径,`z` 沿自转轴量取。它没有改变能量的分量,因此一阶上不影响轨道大小,改变的是轨道朝向。其长期效应为 $$ \begin{aligned} diff --git a/content/concepts/orbit/solar-radiation-pressure.md b/content/concepts/orbit/solar-radiation-pressure.md index 8fe40de..2a6ea1a 100644 --- a/content/concepts/orbit/solar-radiation-pressure.md +++ b/content/concepts/orbit/solar-radiation-pressure.md @@ -14,13 +14,13 @@ in a low Earth orbit model and dominant in a high one. ## Definition **Solar radiation pressure** is the momentum flux carried by sunlight at a -distance `r` from the Sun. It is not a force on a satellite but on the light +distance $r$ from the Sun. It is not a force on a satellite but on the light it intercepts; the acceleration the satellite feels is the flux multiplied by the satellite's area-to-mass ratio and by a reflectivity factor. -The pressure at one astronomical unit is about `4.56e-6 N/m^2`. Divided by +The pressure at one astronomical unit is about $4.56 \times 10^{-6}\,\text{N}/\text{m}^2$. Divided by the area-to-mass ratio of a spacecraft, it gives an acceleration of the same -order as `J_2` on a large satellite at low altitude. +order as $J_2$ on a large satellite at low altitude. ## Mathematics @@ -32,19 +32,19 @@ $$ P(r) = P_{1\,\text{au}} \left( \frac{1\,\text{au}}{r} \right)^2 $$ -with `P_1au = 4.56e-6 N/m^2` for total solar irradiance at 1 au. +with $P_{1\,\text{au}} = 4.56 \times 10^{-6}\,\text{N}/\text{m}^2$ for total solar irradiance at 1 au. ### The acceleration -For a satellite with projected area `A`, mass `m`, and a coefficient -`C_R` that is 1 for absorption, 2 for perfect reflection and between for a +For a satellite with projected area $A$, mass $m$, and a coefficient +$C_R$ that is 1 for absorption, 2 for perfect reflection and between for a typical surface, the acceleration is $$ a_{\text{srp}} = C_R \frac{P_{1\,\text{au}} A}{m} \left( \frac{1\,\text{au}}{r} \right)^2 $$ -The `A/m` ratio is the whole story. It ranges from about `0.002 m^2/kg` for a +The $A/m$ ratio is the whole story. It ranges from about $0.002\,\text{m}^2/\text{kg}$ for a large communications satellite to about `0.05` for a small cube of a CubeSat, a factor of 25 that is entirely attributable to structure. @@ -74,7 +74,7 @@ mean over a long arc. ## Unit conventions -- Flux in watts per square metre; `P_1au = 1361 W/m^2` for total solar +- Flux in watts per square metre; $P_{1\,\text{au}} = 1361\,\text{W}/\text{m}^2$ for total solar irradiance, which corresponds to the pressure above. - Area-to-mass ratio in square metres per kilogram. - Distances in kilometres; the `1 au` ratio is dimensionless so the unit @@ -88,13 +88,13 @@ Earth orbit with a large area-to-mass ratio, the semi-major axis drifts noticeably over a few years, which is why the planning of a constellation in the geosynchronous or high Earth orbit includes radiation pressure in its station-keeping budget. For a low Earth orbit it is a perturbation beside -`J_2` and atmospheric drag. +$J_2$ and atmospheric drag. The mean motion acceleration from the transverse component is what a model should capture. The short-period term is smaller than it looks because it averages, but for a sun-synchronous orbit it is not a perturbation at all, because it is the mechanism: the nodal rate is chosen by tuning it against -`J_2`, and the two balance. +$J_2$, and the two balance. The eclipse term matters only where the radiation pressure is large, and it matters because it is biased rather than symmetric. @@ -108,6 +108,6 @@ matters because it is biased rather than symmetric. ## See also -- [[perturbations]] for the size of the effect beside `J_2` and drag +- [[perturbations]] for the size of the effect beside $J_2$ and drag - [[atmospheric-drag]] for the other century-scale term - [[constellations]] for why a sun-synchronous plane is chosen by balance \ No newline at end of file diff --git a/content/concepts/orbit/solar-radiation-pressure.zh.md b/content/concepts/orbit/solar-radiation-pressure.zh.md index a19fe2a..17ed665 100644 --- a/content/concepts/orbit/solar-radiation-pressure.zh.md +++ b/content/concepts/orbit/solar-radiation-pressure.zh.md @@ -10,9 +10,9 @@ summary: 受阳光照射的表面从光子动量获得的加速度、它为何 ## 定义 -**太阳光压**是距太阳 `r` 处阳光携带的动量通量。它不是作用在卫星上的力,而是作用在卫星所截获的光上的力;卫星感受到的加速度是该通量乘以卫星的面质比与一个反射系数。 +**太阳光压**是距太阳 $r$ 处阳光携带的动量通量。它不是作用在卫星上的力,而是作用在卫星所截获的光上的力;卫星感受到的加速度是该通量乘以卫星的面质比与一个反射系数。 -一个天文单位处的压强约为 `4.56e-6 N/m^2`。除以航天器的面质比,即得与 `J_2` 同量级的加速度。 +一个天文单位处的压强约为 $4.56 \times 10^{-6}\,\text{N}/\text{m}^2$。除以航天器的面质比,即得与 $J_2$ 同量级的加速度。 ## 数学含义 @@ -24,17 +24,17 @@ $$ P(r) = P_{1\,\text{au}} \left( \frac{1\,\text{au}}{r} \right)^2 $$ -其中 1 au 处总太阳辐照度对应 `P_1au = 4.56e-6 N/m^2`。 +其中 1 au 处总太阳辐照度对应 $P_{1\,\text{au}} = 4.56 \times 10^{-6}\,\text{N}/\text{m}^2$。 ### 加速度 -对投影面积 `A`、质量 `m`、系数为 `C_R` 的卫星(完全吸收为 1,完全反射为 2,典型表面介于两者之间),加速度为 +对投影面积 $A$、质量 $m$、系数为 $C_R$ 的卫星(完全吸收为 1,完全反射为 2,典型表面介于两者之间),加速度为 $$ a_{\text{srp}} = C_R \frac{P_{1\,\text{au}} A}{m} \left( \frac{1\,\text{au}}{r} \right)^2 $$ -`A/m` 决定一切。它从大型通信卫星的约 `0.002 m^2/kg` 到立方卫星的约 `0.05`,相差 25 倍,完全归因于结构。 +$A/m$ 决定一切。它从大型通信卫星的约 $0.002\,\text{m}^2/\text{kg}$ 到立方卫星的约 `0.05`,相差 25 倍,完全归因于结构。 ### 三个分量 @@ -52,15 +52,15 @@ $$ ## 单位约定 -- 通量:瓦每平方米;1 au 处总太阳辐照度为 `P_1au = 1361 W/m^2`,与上述压强对应。 +- 通量:瓦每平方米;1 au 处总太阳辐照度为 $P_{1\,\text{au}} = 1361\,\text{W}/\text{m}^2$,与上述压强对应。 - 面质比:平方米每千克。 - 距离:千米;`1 au` 之比为无量纲故单位无关,但由此得到的加速度必须用千米每二次方秒,与轨道一致。 ## 适用边界 -该效应在半长轴上是百年尺度的关注点。对面质比大的高轨,半年内半长轴漂移明显,因此地球静止轨道或高轨星座的规划把辐射压计入其位保预算。对低轨它只是 `J_2` 与大气阻力旁边的一项摄动。 +该效应在半长轴上是百年尺度的关注点。对面质比大的高轨,半年内半长轴漂移明显,因此地球静止轨道或高轨星座的规划把辐射压计入其位保预算。对低轨它只是 $J_2$ 与大气阻力旁边的一项摄动。 -来自横向分量的平均运动加速度是模型应当捕获的。短周期项看起来大,因为它会平均掉;但对太阳同步轨道而言它根本不是摄动,而正是机理:节点速率正是通过调节它与 `J_2` 使二者平衡而选定的。 +来自横向分量的平均运动加速度是模型应当捕获的。短周期项看起来大,因为它会平均掉;但对太阳同步轨道而言它根本不是摄动,而正是机理:节点速率正是通过调节它与 $J_2$ 使二者平衡而选定的。 食项只在辐射压大的地方才重要,而且重要是因为它有偏而非对称。 @@ -72,6 +72,6 @@ $$ ## 另见 -- [[perturbations]]:该效应相对 `J_2` 与阻力的大小 +- [[perturbations]]:该效应相对 $J_2$ 与阻力的大小 - [[atmospheric-drag]]:另一个百年尺度项 - [[constellations]]:太阳同步轨道面为何由平衡选出 \ No newline at end of file diff --git a/content/concepts/physics/earth-rotation-angle.md b/content/concepts/physics/earth-rotation-angle.md index b66f6f1..5405082 100644 --- a/content/concepts/physics/earth-rotation-angle.md +++ b/content/concepts/physics/earth-rotation-angle.md @@ -13,7 +13,7 @@ definition, the expression, and its limits. ## Definition -The Earth Rotation Angle, written `theta` or ERA, is the angle measured +The Earth Rotation Angle, written $\theta$ or ERA, is the angle measured along the true equator from the Celestial Intermediate Origin to the Terrestrial Intermediate Origin. In practice it is the angle between the inertial and Earth-fixed frames, and that is how it is used here. @@ -38,7 +38,7 @@ a solar day. The angle is not reduced to one turn. The value returned is a continuous function of time, so the difference between two angles is meaningful even when the interval is longer than a day. A caller that needs an angle in -`[0, 2 pi)` reduces it explicitly. +$[0, 2\pi)$ reduces it explicitly. The rotation between the frames is a clockwise turn about the third axis: diff --git a/content/concepts/physics/earth-rotation-angle.zh.md b/content/concepts/physics/earth-rotation-angle.zh.md index 5070fe9..15937eb 100644 --- a/content/concepts/physics/earth-rotation-angle.zh.md +++ b/content/concepts/physics/earth-rotation-angle.zh.md @@ -10,7 +10,7 @@ summary: 惯性系与地固系之间的夹角,以及给出该值的线性表 ## 定义 -地球自转角记作 `theta` 或 ERA,是沿真赤道从天体中间原点量到地球中间原点的角度。在实践中它就是惯性系与地固系之间的夹角,本文档也按此使用。 +地球自转角记作 $\theta$ 或 ERA,是沿真赤道从天体中间原点量到地球中间原点的角度。在实践中它就是惯性系与地固系之间的夹角,本文档也按此使用。 它是 UT1(由地球自转定义的时间尺度)的函数。 @@ -24,7 +24,7 @@ $$ 其中 `d` 是自 2000-01-01 12:00 UT1 起 UT1 的天数,结果单位为圈。常数项是历元时刻的角,约 280.46 度。速率略大于每天一圈,因为地球在一个恒星日内转一圈,而恒星日短于太阳日。 -该角不归约到一圈之内。返回的值是时间的连续函数,因此即使区间超过一天,两个角之差仍有意义。需要 `[0, 2 pi)` 内角度的调用方需自行归约。 +该角不归约到一圈之内。返回的值是时间的连续函数,因此即使区间超过一天,两个角之差仍有意义。需要 $[0, 2\pi)$ 内角度的调用方需自行归约。 参考系之间的旋转是绕第三轴的顺时针转动: diff --git a/content/concepts/physics/geodesy-wgs84.md b/content/concepts/physics/geodesy-wgs84.md index 8afa6a2..8737556 100644 --- a/content/concepts/physics/geodesy-wgs84.md +++ b/content/concepts/physics/geodesy-wgs84.md @@ -17,11 +17,11 @@ numbers: | Parameter | Symbol | Value | Unit | |-----------|--------|-------|------| -| Semi-major axis | `a` | 6 378 137.0 | m | -| Inverse flattening | `1/f` | 298.257 223 563 | dimensionless | -| Flattening | `f` | 1 / 298.257 223 563 | dimensionless | -| Semi-minor axis | `b = a (1 - f)` | 6 356 752.314 245 | m | -| First eccentricity squared | `e^2 = 2 f - f^2` | 0.006 694 379 990 13 | dimensionless | +| Semi-major axis | $a$ | 6 378 137.0 | m | +| Inverse flattening | $1/f$ | 298.257 223 563 | dimensionless | +| Flattening | $f$ | 1 / 298.257 223 563 | dimensionless | +| Semi-minor axis | $b = a(1 - f)$ | 6 356 752.314 245 | m | +| First eccentricity squared | $e^2 = 2f - f^2$ | 0.006 694 379 990 13 | dimensionless | Every other quantity is derived from the two defining numbers, so the derived values agree with the published ones to the precision of the @@ -29,10 +29,10 @@ definitions rather than being independently rounded. Three coordinates describe a point: -- **Geodetic latitude** `phi`, the angle between the ellipsoid normal at the +- **Geodetic latitude** $\phi$, the angle between the ellipsoid normal at the point and the equatorial plane. -- **Longitude** `lambda`, positive east of the prime meridian. -- **Height** `h`, measured along the ellipsoid normal, positive outward. +- **Longitude** $\lambda$, positive east of the prime meridian. +- **Height** $h$, measured along the ellipsoid normal, positive outward. Geodetic latitude is not geocentric latitude. Geocentric latitude is the angle between the radius vector to the point and the equatorial plane. The @@ -58,9 +58,9 @@ z &= \left( N (1 - e^2) + h \right) \sin\phi \end{aligned} $$ -`N` is the distance from the point to the rotation axis measured in the -equatorial plane for a point at zero height. It grows from `a` at the -equator to `a / sqrt(1 - e^2)` at the pole. +$N$ is the distance from the point to the rotation axis measured in the +equatorial plane for a point at zero height. It grows from $a$ at the +equator to $a / \sqrt{1 - e^2}$ at the pole. The reverse conversion, from Cartesian coordinates to geodetic latitude and height, has no elementary closed form and is solved by iteration or by a diff --git a/content/concepts/physics/geodesy-wgs84.zh.md b/content/concepts/physics/geodesy-wgs84.zh.md index e89fa24..b6e4fcf 100644 --- a/content/concepts/physics/geodesy-wgs84.zh.md +++ b/content/concepts/physics/geodesy-wgs84.zh.md @@ -14,19 +14,19 @@ WGS 84 椭球是由两个数定义的旋转扁椭球: | 参数 | 符号 | 数值 | 单位 | |------|------|------|------| -| 长半轴 | `a` | 6 378 137.0 | m | -| 扁率倒数 | `1/f` | 298.257 223 563 | 无量纲 | -| 扁率 | `f` | 1 / 298.257 223 563 | 无量纲 | -| 短半轴 | `b = a (1 - f)` | 6 356 752.314 245 | m | -| 第一偏心率平方 | `e^2 = 2 f - f^2` | 0.006 694 379 990 13 | 无量纲 | +| 长半轴 | $a$ | 6 378 137.0 | m | +| 扁率倒数 | $1/f$ | 298.257 223 563 | 无量纲 | +| 扁率 | $f$ | 1 / 298.257 223 563 | 无量纲 | +| 短半轴 | $b = a(1 - f)$ | 6 356 752.314 245 | m | +| 第一偏心率平方 | $e^2 = 2f - f^2$ | 0.006 694 379 990 13 | 无量纲 | 其余所有量都由这两个定义数导出,因此导出的数值与已发布值在定义精度内一致,而不是各自独立舍入的结果。 三个坐标描述一个点: -- **大地纬度** `phi`,该点处椭球法线与赤道面的夹角。 -- **经度** `lambda`,本初子午线以东为正。 -- **椭球高** `h`,沿椭球法线量取,向外为正。 +- **大地纬度** $\phi$,该点处椭球法线与赤道面的夹角。 +- **经度** $\lambda$,本初子午线以东为正。 +- **椭球高** $h$,沿椭球法线量取,向外为正。 大地纬度不是地心纬度。地心纬度是到该点的矢径与赤道面的夹角。二者在赤道与两极一致,在中纬度最大相差约 0.19 度,约合地面 21 公里。 @@ -49,7 +49,7 @@ z &= \left( N (1 - e^2) + h \right) \sin\phi \end{aligned} $$ -`N` 是零高点在赤道面内到自转轴的距离,从赤道的 `a` 增至极点的 `a / sqrt(1 - e^2)`。 +$N$ 是零高点在赤道面内到自转轴的距离,从赤道的 $a$ 增至极点的 $a / \sqrt{1 - e^2}$。 反向转换,即从直角坐标求大地纬度与高,没有初等闭式解,需迭代或级数求解。实现采用在固定步数内收敛到双精度表示精度的方法。当点恰好位于自转轴上时,经度无定义,转换返回空值。 diff --git a/content/concepts/physics/reference-frames.md b/content/concepts/physics/reference-frames.md index bae851a..b95cb9f 100644 --- a/content/concepts/physics/reference-frames.md +++ b/content/concepts/physics/reference-frames.md @@ -37,7 +37,7 @@ $$ \mathbf{r}_{\text{ecef}} = R_3(\theta) \, \mathbf{r}_{\text{eci}}, \qquad \mathbf{r}_{\text{eci}} = R_3(\theta)^{\mathsf{T}} \mathbf{r}_{\text{ecef}} $$ -where `theta` is the Earth Rotation Angle and `R3` is the rotation matrix +where $\theta$ is the Earth Rotation Angle and `R3` is the rotation matrix $$ R_3(\theta) = \begin{bmatrix} \cos\theta & \sin\theta & 0 \\ -\sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} diff --git a/content/concepts/physics/reference-frames.zh.md b/content/concepts/physics/reference-frames.zh.md index c1ba58d..8891416 100644 --- a/content/concepts/physics/reference-frames.zh.md +++ b/content/concepts/physics/reference-frames.zh.md @@ -28,7 +28,7 @@ $$ \mathbf{r}_{\text{ecef}} = R_3(\theta) \, \mathbf{r}_{\text{eci}}, \qquad \mathbf{r}_{\text{eci}} = R_3(\theta)^{\mathsf{T}} \mathbf{r}_{\text{ecef}} $$ -其中 `theta` 是地球自转角,`R3` 是旋转矩阵 +其中 $\theta$ 是地球自转角,`R3` 是旋转矩阵 $$ R_3(\theta) = \begin{bmatrix} \cos\theta & \sin\theta & 0 \\ -\sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} diff --git a/content/concepts/rf/atmospheric-propagation.md b/content/concepts/rf/atmospheric-propagation.md index daa2f56..5ab7540 100644 --- a/content/concepts/rf/atmospheric-propagation.md +++ b/content/concepts/rf/atmospheric-propagation.md @@ -39,8 +39,8 @@ $$ \gamma = \frac{k R^{\alpha}}{\cos\theta} $$ -where `R` is the rain rate, `theta` the zenith angle, and `k` and `alpha` -frequency-dependent. The `1 / cos theta` is why a low-elevation pass is +where $R$ is the rain rate, $\theta$ the zenith angle, and `k` and $\alpha$ +frequency-dependent. The $1 / \cos\theta$ is why a low-elevation pass is severe: at ten degrees from the horizon the path is about six times longer through the layer. diff --git a/content/concepts/rf/atmospheric-propagation.zh.md b/content/concepts/rf/atmospheric-propagation.zh.md index 6a20617..ef39d66 100644 --- a/content/concepts/rf/atmospheric-propagation.zh.md +++ b/content/concepts/rf/atmospheric-propagation.zh.md @@ -26,7 +26,7 @@ $$ \gamma = \frac{k R^{\alpha}}{\cos\theta} $$ -其中 `R` 为雨强、`theta` 为天顶角,`k` 与 `alpha` 依赖频率。`1 / cos theta` 就是低仰角过境严重的原因:在地平线上十度处,穿过该层的路径约长六倍。 +其中 $R$ 为雨强、$\theta$ 为天顶角,`k` 与 $\alpha$ 依赖频率。$1 / \cos\theta$ 就是低仰角过境严重的原因:在地平线上十度处,穿过该层的路径约长六倍。 ### 雨衰落 diff --git a/content/concepts/rf/doppler-and-rate.md b/content/concepts/rf/doppler-and-rate.md index 150013b..776d0d9 100644 --- a/content/concepts/rf/doppler-and-rate.md +++ b/content/concepts/rf/doppler-and-rate.md @@ -18,7 +18,7 @@ motion along the line of sight. **Range rate** is the rate of change of range, and it is the same physical quantity: one is a frequency, the other a velocity. -For a satellite moving at `v_r` radially, at range `R`, the received +For a satellite moving at $v_r$ radially, at range $R$, the received frequency shift is $$ @@ -26,7 +26,7 @@ $$ $$ approaching raises the frequency. The non-relativistic form is adequate for -orbital speeds; the relativistic correction is about one part in `10^13` and +orbital speeds; the relativistic correction is about one part in $10^{13}$ and matters only for a clock discipline experiment. ## Mathematics @@ -40,7 +40,7 @@ $$ \frac{\Delta f}{f} = -\frac{2 v_r}{c} $$ -with `v_r` the range rate. The factor of two is why the same satellite +with $v_r$ the range rate. The factor of two is why the same satellite appears to shift twice as much when tracked in two-way mode, and a range model that ignores it has a factor-of-two error that is easy to miss. @@ -78,7 +78,7 @@ self-referenced. - Frequencies in hertz; a shift is often expressed in parts per billion. - Range rate in metres per second or kilometres per second. - Range in kilometres, with the carrier frequency in hertz, so the factor - `2 c / f` carries its units. + $2c/f$ carries its units. - The factor of two applies to two-way; one-way is the single case. ## Applicability diff --git a/content/concepts/rf/doppler-and-rate.zh.md b/content/concepts/rf/doppler-and-rate.zh.md index 6d742a5..b84ca79 100644 --- a/content/concepts/rf/doppler-and-rate.zh.md +++ b/content/concepts/rf/doppler-and-rate.zh.md @@ -12,13 +12,13 @@ summary: 卫星运动如何改变其被接收到的频率,以及该频移如 **多普勒频移**是沿视线方向的相对运动所引起的观测频率改变。**测距率**是距离的变化率,它是同一个物理量:一个是频率,另一个是速度。 -对沿径向以 `v_r` 运动、距离为 `R` 的卫星,接收频率的偏移为 +对沿径向以 $v_r$ 运动、距离为 $R$ 的卫星,接收频率的偏移为 $$ \frac{\Delta f}{f} = -\frac{v_r}{c} $$ -接近使频率升高。非相对论形式对轨道速度已足够;相对论改正约为 `10^-13` 量级,只在时钟驯服实验中才有意义。 +接近使频率升高。非相对论形式对轨道速度已足够;相对论改正约为 $10^{-13}$ 量级,只在时钟驯服实验中才有意义。 ## 数学含义 @@ -30,7 +30,7 @@ $$ \frac{\Delta f}{f} = -\frac{2 v_r}{c} $$ -其中 `v_r` 为测距率。因子二就是同一颗卫星在双向跟踪下看起来频移加倍的原因,而忽略它的测距模型带有很容易漏掉的二倍误差。 +其中 $v_r$ 为测距率。因子二就是同一颗卫星在双向跟踪下看起来频移加倍的原因,而忽略它的测距模型带有很容易漏掉的二倍误差。 ### 由累积频移得到距离 @@ -54,7 +54,7 @@ $$ - 频率:赫兹;偏移常以十亿分之一表示。 - 测距率:米每秒或千米每秒。 -- 距离:千米,载波频率用赫兹,因子 `2 c / f` 自带单位。 +- 距离:千米,载波频率用赫兹,因子 $2c/f$ 自带单位。 - 因子二适用于双向;单向即单次情形。 ## 适用边界 diff --git a/content/concepts/rf/interference.md b/content/concepts/rf/interference.md index 3f32018..8ec810d 100644 --- a/content/concepts/rf/interference.md +++ b/content/concepts/rf/interference.md @@ -30,7 +30,7 @@ separates a system that fails loudly from one that fails quietly. ### Carrier to noise density The usable quantity is the carrier to noise density ratio, written as -`C/N0`, which is the carrier power in a one hertz bandwidth over the noise +$C/N_0$, which is the carrier power in a one hertz bandwidth over the noise power in the same bandwidth. $$ @@ -39,7 +39,7 @@ $$ A pointing loss, a rain fade or a jammer each reduces it, and the range error scales with the square root of it. A jammer that costs six decibels of -`C/N0` does considerably more than six decibels of ranging accuracy. +$C/N_0$ does considerably more than six decibels of ranging accuracy. ### The threshold @@ -63,7 +63,7 @@ environment: a jammer must reproduce the motion to corrupt it. ## Unit conventions -- Power in decibels relative to one milliwatt; `C/N0` in decibels hertz or +- Power in decibels relative to one milliwatt; $C/N_0$ in decibels hertz or dBHz. - Noise figure dimensionless, in decibels. - Signal strength on an unknown source cannot be converted without a diff --git a/content/concepts/rf/interference.zh.md b/content/concepts/rf/interference.zh.md index f14eb72..2d0fabe 100644 --- a/content/concepts/rf/interference.zh.md +++ b/content/concepts/rf/interference.zh.md @@ -20,13 +20,13 @@ summary: 噪声与相干源的区别——前者逐步劣化测量,后者用 ### 载噪密度比 -可用量是载噪密度比,记作 `C/N0`,即一赫兹带宽内的载波功率除以同带宽噪声功率: +可用量是载噪密度比,记作 $C/N_0$,即一赫兹带宽内的载波功率除以同带宽噪声功率: $$ \frac{C}{N_0} = \frac{P_r G}{k T_{\text{system}} F} $$ -指向损耗、雨衰落或干扰都会降低它,而测距误差按它的平方根标度。代价六分贝 `C/N0` 的干扰造成的测距精度损失远大于六分贝。 +指向损耗、雨衰落或干扰都会降低它,而测距误差按它的平方根标度。代价六分贝 $C/N_0$ 的干扰造成的测距精度损失远大于六分贝。 ### 门限 @@ -40,7 +40,7 @@ $$ ## 单位约定 -- 功率:相对一毫瓦的分贝;`C/N0` 用 dBHz。 +- 功率:相对一毫瓦的分贝;$C/N_0$ 用 dBHz。 - 噪声系数无量纲,以分贝表示。 - 在没有参考的情况下无法把未知源的信号强度换算为绝对量,这正是干扰报告本质上是相对的原因。 diff --git a/content/concepts/rf/link-budget.md b/content/concepts/rf/link-budget.md index d36c76c..af8f64e 100644 --- a/content/concepts/rf/link-budget.md +++ b/content/concepts/rf/link-budget.md @@ -19,8 +19,8 @@ $$ P_r = \frac{\text{EIRP} \, G_t}{L} $$ -with `L` the total propagation and implementation loss. The link closes when -`P_r` exceeds the sensitivity of the receiver, and the **link margin** is the +with $L$ the total propagation and implementation loss. The link closes when +$P_r$ exceeds the sensitivity of the receiver, and the **link margin** is the difference in decibels. Every term is a ratio, so the budget is written in decibels throughout: @@ -77,7 +77,7 @@ part of the orbit. ### Rain attenuation Attenuation from rain at frequencies above 10 GHz is significant. It scales -roughly with `f^0.75` up to about 30 GHz and is then given by the specific +roughly with $f^{0.75}$ up to about 30 GHz and is then given by the specific attenuation `gamma` times the path length through the rain cell. The design figure is not the average attenuation but the value exceeded for a stated fraction of the worst hour in a year, because the budget is closed against diff --git a/content/concepts/rf/link-budget.zh.md b/content/concepts/rf/link-budget.zh.md index 03c9c57..71b95a9 100644 --- a/content/concepts/rf/link-budget.zh.md +++ b/content/concepts/rf/link-budget.zh.md @@ -16,7 +16,7 @@ $$ P_r = \frac{\text{EIRP} \, G_t}{L} $$ -其中 `L` 为全部传播与实现损耗。当 `P_r` 超过接收机灵敏度时链路闭合,**链路裕量**是以分贝计的差值。 +其中 $L$ 为全部传播与实现损耗。当 $P_r$ 超过接收机灵敏度时链路闭合,**链路裕量**是以分贝计的差值。 每一项都是比值,因此预算全程以分贝书写: @@ -60,7 +60,7 @@ $$ ### 雨衰减 -10 GHz 以上频率上雨的衰减显著。它约按 `f^0.75` 增长直到约 30 GHz,此后由比衰减 `gamma` 乘以穿过雨胞的路径长度给出。设计取的不是平均衰减,而是超过最恶劣年份中最差小时某一比例的数值,因为预算是对雨闭合的,不是对中位数闭合的。 +10 GHz 以上频率上雨的衰减显著。它约按 $f^{0.75}$ 增长直到约 30 GHz,此后由比衰减 `gamma` 乘以穿过雨胞的路径长度给出。设计取的不是平均衰减,而是超过最恶劣年份中最差小时某一比例的数值,因为预算是对雨闭合的,不是对中位数闭合的。 ### 指向与指向损耗 diff --git a/docs/design.md b/docs/design.md index fd024b1..b78c87e 100644 --- a/docs/design.md +++ b/docs/design.md @@ -136,9 +136,14 @@ as fenced code blocks, which is what a plain text file allows, and the renderer was made to recognise a block that held equations. That was treating the symptom: a code block is not the notation for mathematics, the reader of the source sees a formula presented as code, and a renderer that guesses which -blocks are formulas is a renderer that guesses wrong. The blocks are now -written in the notation they should always have used, and the renderer reads -the one notation every Markdown mathematics tool reads. +blocks are formulas is a renderer that guesses wrong. + +The guess was removed rather than refined. It was wrong in practice and not +only in principle: a doc comment in the library carries a Rust example in a +fence with no language on it, and the example holds an equals sign, so the +page drew a program as a formula. A code block is now drawn as a code block +and nothing else, and the blocks in the documents are written in the +notation they should always have used. Setting a formula is mostly an exercise in not losing a character. Text inside a MathML row with no element around it is not drawn, so a token that is not diff --git a/docs/design.zh.md b/docs/design.zh.md index 97d6b9c..60f1ef8 100644 --- a/docs/design.zh.md +++ b/docs/design.zh.md @@ -98,9 +98,12 @@ MathML。 文档并非一直都是这么写的。起初它们写成代码块——纯文本文件里能用的写法——而 渲染器被改成能认出「装着等式的代码块」。那是治标:代码块不是数学的记法,读源码的 -人看到的是被当作代码呈现的公式,而靠猜哪些块是公式的渲染器一定会猜错。这些块 -现在改用它们本就该用的记法书写,渲染器也只读那一种所有 Markdown 数学工具都读的 -记法。 +人看到的是被当作代码呈现的公式,而靠猜哪些块是公式的渲染器一定会猜错。 + +这个猜测是被删除的,而不是被改进的。它不仅原理上错,实践中也真的错:库中一段 +文档注释用不带语言标记的围栏写了一个 Rust 例子,而例子里有等号,于是页面把一个 +程序当成了公式来画。现在代码块就只按代码块绘制,而文档中的公式块改用它们本就 +该用的记法书写。 排版公式基本就是一件「别丢字符」的事。MathML 行内没有元素包裹的文本不会被绘制, 因此没有元素的 token 会消失,留下一个看起来合理却是错的公式。所以每一段文本都被 diff --git a/src/docs/markdown.rs b/src/docs/markdown.rs index 2f855ae..f278ed4 100644 --- a/src/docs/markdown.rs +++ b/src/docs/markdown.rs @@ -68,13 +68,7 @@ pub fn render_with(markdown: &str, items: Option<&std::collections::HashSet output.push_str(&rendered), - None => output.push_str(&render_code_block(language, &code)), - } + output.push_str(&render_code_block(language, &code)); continue; } @@ -234,19 +228,6 @@ fn collect_table<'a>(lines: &[&'a str], start: usize) -> (Vec<&'a str>, usize) { } /// Renders a fenced code block. -/// Renders a fenced block that holds equations, when it does. -/// -/// Only a block marked `text`, or marked with nothing at all, is considered. -/// A block marked as a language is source code by the author's statement, and -/// a shell transcript or a listing is not a formula however many equals signs -/// it happens to contain. -fn render_formula_block(language: &str, code: &str) -> Option { - if !language.is_empty() && language != "text" { - return None; - } - crate::docs::mathml::render_plain(code).map(|math| format!("{math}\n")) -} - fn render_code_block(language: &str, code: &str) -> String { let class = if language.is_empty() { String::new() diff --git a/src/docs/mathml.rs b/src/docs/mathml.rs index c111a0d..f9a0f60 100644 --- a/src/docs/mathml.rs +++ b/src/docs/mathml.rs @@ -121,10 +121,6 @@ enum Node { Subscript(Box, Box), /// A subscript and a superscript on one base. Scripts(Box, Box, Box), - /// A quotient drawn with a slash rather than stacked. - Slash(Box, Box), - /// A subscript on a subscript, as the `1` in the `v_1` of `delta_v_1`. - Nested(Box, Box), /// A named function such as `sin`. Function(String), /// A mark written above its argument, as in `\dot\Omega`. @@ -838,538 +834,5 @@ fn emit(node: &Node) -> String { emit(bottom), emit(top) ), - // A stacked fraction would be as tall as the line it is in, so a - // quotient inside a formula is drawn with a slash and takes the - // height of its parts. - Node::Slash(top, bottom) => format!( - "{}{}", - emit(top), - emit(bottom) - ), - Node::Nested(base, inner) => format!( - "{}{}", - emit(base), - emit(inner) - ), - } -} - -/// Renders a formula written in plain ASCII notation. -/// -/// The documents were written before the formula support existed, and they -/// wrote formulas the way a plain text file allows: a fenced block holding -/// `delta_v_1 = sqrt( mu (2/r_1 - 1/a_t) )`. Rendering those as formulas is -/// what makes them formulas, so this reads that notation and produces the -/// same MathML as the dollar form. -/// -/// The notation understood is the one the documents use: -/// -/// - an identifier or a number, which may carry a subscript and a -/// superscript, as in `a_t^2` or `e^2`; -/// - `sqrt(...)` for a root, and `sin`, `cos`, `tan`, `ln`, `exp`, `floor` -/// and `arctan` among others for the functions; -/// - `*` for a product, which is written by juxtaposition; -/// - `/` for a quotient, which is drawn with a slash rather than stacked, so -/// that `2/r_1` does not become a fraction as tall as the line it is in; -/// - `+ - = < >` as they are. -/// -/// The reading is a plain precedence climb: a sum, then a product, then a -/// factor. That is what makes `2/r_1 - 1/a_t` two quotients rather than one -/// quotient of the whole difference. -/// -/// Returns `None` when the text is not a formula. A block holding a matrix, or -/// prose, or something not fully read is not a formula and must stay as it was -/// written: showing part of a formula that was not understood would be worse -/// than showing the source. -pub fn render_plain(text: &str) -> Option { - let lines: Vec<&str> = text - .lines() - .map(str::trim_end) - .filter(|line| !line.trim().is_empty()) - .collect(); - if lines.is_empty() { - return None; - } - - // A block qualifies as a formula when at least one of its lines is an - // equation. A line that turns out to be a matrix or a vector is shown as - // the text it was written, and does not stop the equations beside it from - // being set as formulas. - if !lines.iter().any(|line| line.contains('=')) { - return None; - } - - let mut rows = String::new(); - let mut read = 0; - for line in &lines { - // A bracket in these documents is a matrix or a vector, which this - // renderer does not lay out. Such a line is shown as the text it was - // written, and the equations beside it are still set as formulas: a - // block often gives an equation and then the vector it acts on. - if line.contains('[') || line.contains(']') { - rows.push_str(""); - rows.push_str(&escape(&clean_note(line))); - rows.push_str(""); - continue; - } - let (equation, note) = split_trailing_note(line, reads_whole); - let tokens = tokenize_ascii(equation); - let mut cursor = 0; - let nodes = parse_sum(&tokens, &mut cursor); - // A line that could not be read whole is still shown, but as the text - // it was written. A block of equations often carries a continuation - // line with no equals sign on it, and losing that would lose the - // equation above it. - if cursor != tokens.len() { - rows.push_str(""); - rows.push_str(&escape(&clean_note(equation))); - rows.push_str(""); - continue; - } - read += 1; - rows.push_str(""); - rows.push_str(&emit(&Node::Row(nodes))); - if let Some(note) = note { - // The note is a phrase about the equation rather than part of it, - // so it is set beside the formula and not inside it. - rows.push_str(""); - rows.push_str(&escape(&clean_note(note))); - rows.push_str(""); - } - rows.push_str(""); - } - - // Nothing in the block could be read, so the caller is better served by - // the source than by a block of text set as mathematics. - if read == 0 { - return None; - } - - Some(format!( - "\ - \ - {rows}", - escape_attr(text.trim()) - )) -} - -/// Tidies a note read out of an equation line. -/// -/// The note was written in the same block as the equation, so it carries the -/// alignment of the block and runs on several lines; what is left after the -/// spaces are collapsed is a phrase. -fn clean_note(note: &str) -> String { - note.split_whitespace().collect::>().join(" ") -} - -/// Reports whether a line reads out as a formula in its entirety. -fn reads_whole(line: &str) -> bool { - let tokens = tokenize_ascii(line); - let mut cursor = 0; - parse_sum(&tokens, &mut cursor); - cursor == tokens.len() -} - -/// Splits a trailing parenthetical note from an equation. -/// -/// A note is a phrase about the equation, as in `(prime vertical radius)`, -/// and the authors wrote it out of the way of the equation: it starts in a -/// column of its own, several spaces along. That spacing is what tells it -/// apart from a bracket that belongs to the equation, such as the -/// `(cos theta)` in `gamma = k * R^alpha / (cos theta)`, which follows a -/// single space. -/// -/// The spacing alone is not trusted: the rest of the line must also read out -/// as an expression without the note. A parenthesis of several words reads as -/// a product of names, so `prime vertical radius` on its own would be -/// accepted by the reader; what marks it is that it is set apart. -fn split_trailing_note(line: &str, reads: impl Fn(&str) -> bool) -> (&str, Option<&str>) { - let trimmed = line.trim_end(); - if !trimmed.ends_with(')') { - return (trimmed, None); - } - let Some(start) = trimmed.rfind('(') else { - return (trimmed, None); - }; - if start == 0 { - return (trimmed, None); - } - // The gap before the parenthesis, counted back to the last non-space. - let gap = trimmed[..start] - .chars() - .rev() - .take_while(|c| c.is_whitespace()) - .count(); - if gap < 2 { - return (trimmed, None); - } - let inside = &trimmed[start + 1..trimmed.len() - 1]; - if !inside.contains(' ') || !reads(trimmed[..start].trim_end()) { - return (trimmed, None); - } - (trimmed[..start].trim_end(), Some(inside)) -} - -/// One unit of a plain formula. -#[derive(Debug, Clone, PartialEq, Eq)] -enum Ascii { - /// An identifier: a run of letters, digits and underscores. - Word(String), - /// A number, possibly with a decimal point or an exponent. - Number(String), - /// An operator, a bracket or a mark. - Sign(char), -} - -/// Reads a plain formula into tokens. -fn tokenize_ascii(text: &str) -> Vec { - let characters: Vec = text.chars().collect(); - let mut tokens: Vec = Vec::with_capacity(characters.len()); - let mut index = 0; - - while index < characters.len() { - let character = characters[index]; - if character.is_whitespace() { - index += 1; - continue; - } - if character.is_alphabetic() { - let start = index; - while index < characters.len() - && (characters[index].is_alphanumeric() - || characters[index] == '_' - || characters[index] == '\'') - { - index += 1; - } - tokens.push(Ascii::Word(characters[start..index].iter().collect())); - continue; - } - if character.is_ascii_digit() || character == '.' { - let start = index; - while index < characters.len() && characters[index].is_ascii_digit() { - index += 1; - } - if index < characters.len() && characters[index] == '.' { - index += 1; - while index < characters.len() && characters[index].is_ascii_digit() { - index += 1; - } - } - // An exponent is part of the number, as in `1e-6`. - if index < characters.len() - && matches!(characters[index], 'e' | 'E') - && characters - .get(index + 1) - .is_some_and(|next| next.is_ascii_digit() || *next == '-') - { - index += 1; - if characters[index] == '-' { - index += 1; - } - while index < characters.len() && characters[index].is_ascii_digit() { - index += 1; - } - } - tokens.push(Ascii::Number(characters[start..index].iter().collect())); - continue; - } - tokens.push(Ascii::Sign(character)); - index += 1; - } - tokens -} - -/// The functions the documents write in plain ASCII. -fn ascii_function(name: &str) -> bool { - matches!( - name, - "sin" - | "cos" - | "tan" - | "cot" - | "sec" - | "csc" - | "asin" - | "acos" - | "atan" - | "arctan" - | "sinh" - | "cosh" - | "tanh" - | "ln" - | "log" - | "log10" - | "exp" - | "floor" - | "ceil" - | "abs" - ) -} - -/// Parses until a closing vertical bar, which ends an absolute value. -/// -/// The bar is not a bracket as such: it closes only the bar that opened the -/// run, and the tokens between are an ordinary expression. -fn parse_sum_until_bar(tokens: &[Ascii], cursor: &mut usize) -> Vec { - let mut row = parse_product(tokens, cursor, true); - while let Some(Ascii::Sign(operator @ ('+' | '-'))) = tokens.get(*cursor) { - let operator = *operator; - *cursor += 1; - let right = parse_product(tokens, cursor, true); - row.push(Node::Text( - if operator == '+' { " + " } else { " − " }.to_string(), - )); - row.extend(right); - } - row -} - -/// Parses a sum: factors joined by `+`, `-`, `=` or a comparison. -fn parse_sum(tokens: &[Ascii], cursor: &mut usize) -> Vec { - let mut row = parse_product(tokens, cursor, false); - while let Some(Ascii::Sign(operator @ ('+' | '-' | '=' | '<' | '>' | ','))) = - tokens.get(*cursor) - { - let operator = *operator; - *cursor += 1; - let right = parse_product(tokens, cursor, false); - row.push(Node::Text(match operator { - '=' => " = ".to_string(), - '<' => " < ".to_string(), - '>' => " > ".to_string(), - '+' => " + ".to_string(), - // Two equations are often written on one line, separated by a - // comma, as in `r = p / (1 + e cos nu), p = a (1 - e^2)`. - ',' => ", ".to_string(), - _ => " − ".to_string(), - })); - row.extend(right); - } - row -} - -/// Parses a product: factors joined by `/`, by `*`, or by nothing at all. -/// -/// A product written in these documents is usually juxtaposition, as in -/// `2 f` or `mu (2/r_1 - 1/a_t)`, so a factor that follows another with -/// nothing between them continues the product. The star, where it is written, -/// is dropped. -/// `inside_bars` stops the loop at a closing vertical bar. -/// -/// Inside an absolute value a bar is the end of the expression, while outside -/// one it opens the next factor of a product, as the `| x|` in -/// `| f'(x) | * | x|`. -fn parse_product(tokens: &[Ascii], cursor: &mut usize, inside_bars: bool) -> Vec { - let mut row = parse_factor(tokens, cursor); - loop { - match tokens.get(*cursor).cloned() { - Some(Ascii::Sign('*')) => { - *cursor += 1; - } - Some(Ascii::Sign('/')) => { - *cursor += 1; - let bottom = parse_factor(tokens, cursor); - // The quotient takes the factor before it as its top and the - // one after it as its bottom, so `2/r_1` is one quotient and - // not a quotient of whatever happened to precede it. - let top = row.split_off(row.len() - 1); - row.push(Node::Slash( - Box::new(Node::Row(top)), - Box::new(Node::Row(bottom)), - )); - } - // Two factors with nothing between them are a product. - Some(Ascii::Word(_)) | Some(Ascii::Number(_)) | Some(Ascii::Sign('(')) => { - row.extend(parse_factor(tokens, cursor)); - } - Some(Ascii::Sign('|')) if !inside_bars => { - row.extend(parse_factor(tokens, cursor)); - } - _ => return row, - } - } -} - -/// Parses one factor: a value with its scripts, a root, a call, or a group. -fn parse_factor(tokens: &[Ascii], cursor: &mut usize) -> Vec { - match tokens.get(*cursor).cloned() { - Some(Ascii::Sign(open @ ('(' | '{'))) => { - let close = if open == '(' { ')' } else { '}' }; - *cursor += 1; - let inner = parse_sum(tokens, cursor); - if matches!(tokens.get(*cursor), Some(Ascii::Sign(closed)) if *closed == close) { - *cursor += 1; - } - // A brace holds a group without showing brackets; a parenthesis - // shows them, because it is written in the source. - if open == '{' { - vec![attach_power(Node::Row(inner), tokens, cursor)] - } else { - let group = Node::Row(vec![ - Node::Text("(".to_string()), - Node::Row(inner), - Node::Text(")".to_string()), - ]); - // A bracketed group may be raised, as in `(4 pi d f / c)^2`. - vec![attach_power(group, tokens, cursor)] - } - } - // A word in a script that is not the Latin one is a label written in - // the line's own language, as in `近地点半径 r_p = a (1 - e)`. It is set - // upright, because a label is not a variable. - Some(Ascii::Word(word)) if !word.is_ascii() => { - *cursor += 1; - vec![Node::Text(word)] - } - Some(Ascii::Word(word)) => { - *cursor += 1; - if word == "sqrt" { - let radicand = parse_group(tokens, cursor); - return vec![attach_power( - Node::Root(Box::new(radicand), None), - tokens, - cursor, - )]; - } - if ascii_function(&word) { - let inner = parse_group(tokens, cursor); - let argument = match inner { - Node::Row(nodes) => Node::Row(nodes), - other => Node::Row(vec![other]), - }; - let call = Node::Row(vec![ - Node::Function(word), - Node::Text(" ".to_string()), - Node::Row(vec![ - Node::Text("(".to_string()), - argument, - Node::Text(")".to_string()), - ]), - ]); - // A function may be raised, as in `sin^2 phi`. The power - // belongs to the whole function rather than to its argument. - return vec![attach_power(call, tokens, cursor)]; - } - let base = ascii_atom(&word, tokens, cursor); - vec![base] - } - Some(Ascii::Number(number)) => { - *cursor += 1; - let base = ascii_atom(&number, tokens, cursor); - vec![base] - } - // A vertical bar opens an absolute value, which the documents write - // as `| f'(x) |` and as `|r|^3`. The closing bar is written with - // its partner, because which bar closes depends on the nesting. - Some(Ascii::Sign('|')) => { - *cursor += 1; - let inner = parse_sum_until_bar(tokens, cursor); - if matches!(tokens.get(*cursor), Some(Ascii::Sign('|'))) { - *cursor += 1; - } - // The magnitude may be raised, as in `|r|^3`. - let magnitude = Node::Row(vec![ - Node::Text("|".to_string()), - Node::Row(inner), - Node::Text("|".to_string()), - ]); - vec![attach_power(magnitude, tokens, cursor)] - } - other => { - // Any other token is read past, so that the parse always makes - // progress rather than stalling on it. - *cursor += 1; - if let Some(Ascii::Sign(character)) = other { - vec![Node::Text(character.to_string())] - } else { - Vec::new() - } - } - } -} - -/// Raises a value to the power that follows it, if there is one. -fn attach_power(value: Node, tokens: &[Ascii], cursor: &mut usize) -> Node { - match tokens.get(*cursor) { - Some(Ascii::Sign('^')) => { - *cursor += 1; - Node::Superscript(Box::new(value), Box::new(parse_group(tokens, cursor))) - } - _ => value, - } -} - -/// Reads a bracketed argument, or a single token when there are no brackets. -fn parse_group(tokens: &[Ascii], cursor: &mut usize) -> Node { - match tokens.get(*cursor).cloned() { - Some(Ascii::Sign(open @ ('(' | '{'))) => { - let close = if open == '(' { ')' } else { '}' }; - *cursor += 1; - let inner = parse_sum(tokens, cursor); - if matches!(tokens.get(*cursor), Some(Ascii::Sign(closed)) if *closed == close) { - *cursor += 1; - } - Node::Row(inner) - } - Some(Ascii::Word(word)) => { - *cursor += 1; - Node::Text(word) - } - Some(Ascii::Number(number)) => { - *cursor += 1; - Node::Text(number) - } - _ => Node::Text(String::new()), - } -} - -/// Builds the node for one identifier or number, with its scripts. -/// -/// A name is written with an underscore between its parts, as in `delta_v_1` -/// or `right_ascension`, so the name is read up to the first underscore and -/// what follows is the subscript. The subscript may carry its own subscript, -/// as `v_1` does. -fn ascii_atom(word: &str, tokens: &[Ascii], cursor: &mut usize) -> Node { - let (base, subscript) = match word.find('_') { - Some(index) => ( - word[..index].to_string(), - Some(word[index + 1..].to_string()), - ), - None => (word.to_string(), None), - }; - let base = Node::Text(base); - - // `E_{n+1}` leaves the subscript to a brace rather than to the name, so an - // underscore with nothing after it is completed from the next token, which - // is the brace group that follows it. - let subscript = match subscript { - Some(text) if text.is_empty() => Some(match parse_group(tokens, cursor) { - Node::Row(nodes) => Node::Row(nodes), - other => Node::Row(vec![other]), - }), - Some(text) => Some(match text.rfind('_') { - Some(index) => Node::Nested( - Box::new(Node::Text(text[..index].to_string())), - Box::new(Node::Text(text[index + 1..].to_string())), - ), - None => Node::Text(text), - }), - None => None, - }; - - let superscript = match tokens.get(*cursor) { - Some(Ascii::Sign('^')) => { - *cursor += 1; - Some(parse_group(tokens, cursor)) - } - _ => None, - }; - - match (subscript, superscript) { - (None, None) => base, - (Some(bottom), None) => Node::Subscript(Box::new(base), Box::new(bottom)), - (None, Some(top)) => Node::Superscript(Box::new(base), Box::new(top)), - (Some(bottom), Some(top)) => Node::Scripts(Box::new(base), Box::new(bottom), Box::new(top)), } } diff --git a/tests/docs_mathml.rs b/tests/docs_mathml.rs index 4842fce..49d31be 100644 --- a/tests/docs_mathml.rs +++ b/tests/docs_mathml.rs @@ -85,218 +85,6 @@ fn the_source_is_kept_in_the_output() { assert!(render("x^2", false).contains("data-tex=\"x^2\"")); } -#[test] -fn the_rocket_equation_is_read() { - // The form the documents write. - let html = render_plain("delta_v = v_e * ln (m_0 / m_f)").expect("a formula"); - assert!(html.contains(""), "no root here"); -} - -#[test] -fn a_subscript_within_a_subscript_is_read() { - // `delta_v_1` is delta with a subscript v, itself carrying 1. - let html = render_plain("delta_v_1 = 3").expect("a formula"); - assert!(html.contains("delta"), "{html}"); - assert!(html.contains(""), "the subscript was lost: {html}"); -} - -#[test] -fn a_power_is_read() { - let html = render_plain("e^2 = 2 f - f^2").expect("a formula"); - assert!(html.contains(""), "the power was lost: {html}"); -} - -#[test] -fn a_root_is_read() { - let html = render_plain("x = sqrt( mu / r )").expect("a formula"); - assert!(html.contains(""), "the root was lost: {html}"); -} - -#[test] -fn a_quotient_is_taken_as_its_own() { - // `2/r_1 - 1/a_t` is two quotients, not one quotient of a difference. - let html = render_plain("d = 2/r_1 - 1/a_t").expect("a formula"); - assert_eq!(html.matches("").count(), 3, "{html}"); -} - -#[test] -fn a_matrix_stays_as_written() { - // A matrix is not a formula this renderer lays out. - assert!(render_plain("R = [ 1 0\n 0 1 ]").is_none()); -} - -#[test] -fn prose_stays_as_written() { - assert!(render_plain("e_max such that the elevation is arctan").is_none()); - assert!(render_plain("just some text").is_none()); -} - -#[test] -fn a_trailing_note_is_not_part_of_the_equation() { - // The parenthesis is a phrase, not a bracket in the expression. - let html = render_plain("N = a / sqrt(1 - e^2 sin^2 phi) (prime vertical radius)") - .expect("a formula"); - // The note is set beside the formula rather than inside it, and it is - // kept rather than dropped. - assert!( - html.contains("prime vertical radius"), - "the note was lost: {html}" - ); - assert!( - html.contains(""), "{html}"); -} - -#[test] -fn a_brace_holds_a_subscript_of_several_symbols() { - // The Newton iteration, written the way the documents write it. - let html = render_plain("E_{n+1} = E_n - f(E_n)").expect("a formula"); - assert!(html.contains(""), "the subscript was lost: {html}"); -} - -#[test] -fn a_continuation_line_is_kept_though_it_is_not_an_equation() { - // A block of equations often carries a line with no equals sign on it. - // Losing that line would lose the equation above it. - let html = render_plain("JDN = day + 365 y\n + 153 m").expect("a formula"); - assert!(html.contains("JDN"), "{html}"); - assert!(html.contains("153 m"), "the continuation was lost: {html}"); -} - -#[test] -fn a_block_with_no_readable_equation_is_left_alone() { - assert!(render_plain("leap(y) = (y mod 4 == 0) and (y mod 100 != 0").is_none()); -} - -#[test] -fn an_absolute_value_may_hold_a_product() { - // `| f'(x) | * | x| / | f(x) |` is one expression: a bar inside it - // opens the next factor rather than closing the one before. - let html = render_plain("kappa = | f'(x) | * | x| / | f(x) |").expect("a formula"); - assert!(!html.contains("math-note"), "a note was read: {html}"); -} - -#[test] -fn a_power_may_be_a_word() { - // `R^alpha` writes the exponent as a name rather than a digit. - assert!(render_plain("gamma = k * R^alpha / (cos theta)").is_some()); -} - -#[test] -fn a_trailing_bracket_of_the_equation_is_not_a_note() { - // `(cos theta)` is part of the equation even though it holds only - // words; the whole line reads out, so there is no note. - let html = render_plain("gamma = k * R^alpha / (cos theta)").expect("a formula"); - assert!(!html.contains("math-note"), "a note was read: {html}"); -} - -#[test] -fn a_bracketed_group_is_not_mistaken_for_a_note() { - // `(m * g)` holds an operator, so it is part of the equation; a note - // is a phrase and has none. - let html = render_plain("H = k * T / (m * g)").expect("a formula"); - assert!(html.contains("\u{2212}"), - "the minus was lost: {html}" - ); - assert!(html.contains("="), "the equals was lost: {html}"); -} - -#[test] -fn an_equals_between_two_equations_is_kept() { - let html = render_plain("r = p / (1 + e cos nu), p = a (1 - e^2)").expect("a formula"); - assert!(html.contains("="), "{html}"); - assert!(html.contains("\u{2212}"), "{html}"); -} -#[test] -fn a_number_of_several_digits_is_kept() { - // Only single digits were given an element, and text without one is - // dropped by the browser, so `14`, `4800` and `32045` vanished from - // every formula that held them. - let html = render_plain("JDN = day + floor((153 m + 2) / 5) + 365 y").expect("a formula"); - for number in ["153", "2", "5", "365"] { - assert!( - html.contains(&format!("{number}")), - "{number} has no element: {html}" - ); - } - // Each number is inside an element; text without one is not drawn. - assert!(html.matches("").count() >= 4, "{html}"); -} - -#[test] -fn a_decimal_and_an_exponent_are_single_numbers() { - let html = render_plain("x = 2400000.5 + 1.5e-3").expect("a formula"); - assert!(html.contains("2400000.5"), "{html}"); - assert!(html.contains("1.5e-3"), "{html}"); -} - -#[test] -fn a_row_holds_elements_rather_than_text() { - // Bare text inside a row is dropped by the browser, so a number or a name - // has to be given an element. The end to end check compares the - // characters the reader sees against the source; this one states the - // shape that makes that true, so a failure points at the cause. - let html = render_plain("a = floor((14 - month) / 12)").expect("a formula"); - let body = html.split("'); - let tag = parts.next().unwrap_or_default(); - let after = parts.next().unwrap_or_default(); - let text: String = after.chars().take_while(|c| *c != '<').collect(); - if text.is_empty() { - continue; - } - let name = tag - .trim_start_matches('/') - .split(' ') - .next() - .unwrap_or_default(); - assert!( - matches!(name, "mi" | "mo" | "mn" | "mtext"), - "text {text:?} sits in <{name}>, which does not draw it: {html}" - ); - } -} - #[test] fn a_bracketed_group_takes_the_power_that_follows_it() { // The bracket branches returned before the scripts were attached, so the diff --git a/tests/formula_rendering.rs b/tests/formula_rendering.rs index 1565798..39f15cc 100644 --- a/tests/formula_rendering.rs +++ b/tests/formula_rendering.rs @@ -29,28 +29,54 @@ use apsis_book::docs::document::Language; -/// Returns the text a reader sees, with the markup removed. +/// Returns the text a reader sees. /// -/// MathML draws an element's text and drops text that has no element, so -/// keeping only what is inside an element is what the browser shows. +/// A browser draws the text of a leaf element and drops text that has no +/// element around it, so this counts the same and no more. An earlier +/// version counted any text inside any element, which made the check below +/// pass whatever the renderer produced: text with no element was counted as +/// drawn although the browser shows nothing. fn visible(html: &str) -> String { + /// The elements whose text is drawn. + const LEAF: [&str; 6] = ["mi", "mo", "mn", "mtext", "mspace", "ms"]; + let mut out = String::new(); - let mut depth: usize = 0; - let mut in_tag = false; + // The elements currently open, innermost last. + let mut open: Vec = Vec::new(); let mut characters = html.chars().peekable(); - while let Some(character) = characters.next() { + let mut in_tag = false; + let mut tag = String::new(); + + for character in characters.by_ref() { match character { '<' => { in_tag = true; - if characters.peek() == Some(&'/') { - depth = depth.saturating_sub(1); - } else { - depth += 1; + tag.clear(); + } + '>' => { + in_tag = false; + let name: String = tag + .trim_start_matches('/') + .split_whitespace() + .next() + .unwrap_or("") + .to_lowercase(); + if tag.starts_with('/') { + open.pop(); + } else if !tag.ends_with('/') { + open.push(name); + } + } + _ if in_tag => tag.push(character), + _ => { + // Drawn only when the innermost open element holds text. + if open + .last() + .is_some_and(|name| LEAF.contains(&name.as_str())) + { + out.push(character); } } - '>' => in_tag = false, - _ if !in_tag && depth > 0 => out.push(character), - _ => {} } } out @@ -163,11 +189,15 @@ fn every_formula_shows_every_character_it_was_written_with() { // The source as written, without the spaces that only set it // out on the page. let written = content_characters(&source); - // The part of the page that is the formula itself. - let start = html.find(&format!("data-tex=\"{source}\"")).unwrap_or(0); + // The part of the page that is the formula itself: the + // MathML and nothing else. The source is carried beside it + // for a reader to copy, and including it here would compare + // the source against itself and pass whatever happened. + let from = html.find(&format!("data-tex=\"{source}\"")).unwrap_or(0); + let start = html[from..].find("") - .map(|e| start + e) + .find("") + .map(|e| start + e + "".len()) .unwrap_or(html.len()); let shown: String = visible(&html[start..end]) .chars()