docs: give every note an English and a Chinese version

The notes were Chinese only, so a reader arriving from GitHub, which
renders README.md by default, met a wall. Each note now follows the
convention the wider ecosystem uses: English in X.md and Simplified
Chinese in X.zh.md, with a switch at the top of both, the way
zeromicro/go-zero and alist-org/alist present theirs.

The Chinese text moved to the .zh.md name rather than being rewritten, so
nothing was lost, and the English is a translation of the same content
rather than a summary of it.

Two figures in the notes had gone stale and are corrected while
translating: there are thirteen expect assertions rather than twelve,
because apsis-math/src/parallel.rs added one, and the table listed eight
of them rather than all. The layer note no longer quotes an edge count,
which changed as the code grew and was already out of date.
This commit is contained in:
ApsisRS contributors 2026-09-30 19:23:26 +08:00
parent 2acc2170ad
commit e5cdb29c82
16 changed files with 1580 additions and 506 deletions

137
README.md
View File

@ -1,110 +1,131 @@
# ApsisRS / 拱点 # ApsisRS
商业航天与卫星互联网领域的 Rust 计算内核。 Computational kernels for commercial spaceflight and satellite internet, in Rust.
## 定位 English | [简体中文](./README.zh.md)
- 纯 Rust、`std`、零第三方 crate 依赖 ## Scope
- 领域边界清晰,按层单向依赖
- 对外门面 crate 名为 `apsis-rs`,库名为 `apsis`
- 当前阶段所有 crate 均 `publish = false`,不执行发布
## 分层 - Pure Rust, standard library only, no third-party crate dependency
- Clear domain boundaries with a one-way dependency order between layers
- The public facade crate is `apsis-rs`; its library name is `apsis`
- Every crate is `publish = false` at this stage, so none of them is released
## Layers
```text ```text
foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs
``` ```
约束: The rules:
- 依赖只能从左向右。下层可依赖其上任意层,上层不得依赖其下任意层。 - Dependencies flow to the left only. A lower layer may use any layer to its
例如 `network` 可以依赖 `orbit`(向左),但 `orbit` 不得依赖 `network` left; a higher layer may not use any layer to its right. So `network` may
(向右,即反向)。左向依赖不限层数,允许跨层,因为把 `orbit` 的值 use `orbit`, but `orbit` may not use `network`. A leftward dependency may
传到 `network` 时中间层未必提供中转类型。 cross as many layers as it needs, because the layers in between need not
- `foundation` 的 `apsis-core` 是全局基准,任何层任何 crate 均可依赖它。 offer a type that carries the value forward.
`apsis-math` 同理,因为线性代数被各层共用。 - `apsis-core` in `foundation` is the global baseline that any crate in any
- 层内以该层的门面 crate(`apsis-orbit`、`apsis-rf`、`apsis-sim`)作为整合点。 layer may use. `apsis-math` is the same, because linear algebra is shared
across the layers.
- Within a layer, that layer's facade crate (`apsis-orbit`, `apsis-rf`,
`apsis-sim`) is the integration point.
这套规则由 `scripts/check-layers.sh` 强制,CI 会运行它。 `scripts/check-layers.sh` enforces these rules and CI runs it.
各层内容: | Layer | Responsibility | Crates |
| 层 | 职责 | crates |
| --- | --- | --- | | --- | --- | --- |
| foundation | 基础类型、单位、数学、宏 | `apsis-core`、`apsis-units`、`apsis-math`、`apsis-macros` | | foundation | Base types, units, mathematics, macros | `apsis-core`, `apsis-units`, `apsis-math`, `apsis-macros` |
| physics | 时间、闰秒、坐标、大地基准 | `apsis-time`、`apsis-leap`、`apsis-coord`、`apsis-geodesy`、`apsis-frames` | | physics | Time, leap seconds, coordinates, datum | `apsis-time`, `apsis-leap`, `apsis-coord`, `apsis-geodesy`, `apsis-frames` |
| orbit | TLE、SGP4、二体、星历、机动 | `apsis-tle`、`apsis-sgp4`、`apsis-kepler`、`apsis-ephemeris`、`apsis-maneuver`、`apsis-orbit` | | orbit | TLE, SGP4, two-body, ephemeris, maneuvers | `apsis-tle`, `apsis-sgp4`, `apsis-kepler`, `apsis-ephemeris`, `apsis-maneuver`, `apsis-orbit` |
| rf | 射频、天线、链路预算 | `apsis-rf`、`apsis-antenna`、`apsis-doppler`、`apsis-atmosphere`、`apsis-noise`、`apsis-interference`、`apsis-link` | | rf | Radio frequency, antennas, link budget | `apsis-rf`, `apsis-antenna`, `apsis-doppler`, `apsis-atmosphere`, `apsis-noise`, `apsis-interference`, `apsis-link` |
| network | 星座、覆盖、接入、拓扑、路由 | `apsis-constellation`、`apsis-coverage`、`apsis-access`、`apsis-topology`、`apsis-isl`、`apsis-routing`、`apsis-handover`、`apsis-mesh` | | network | Constellations, coverage, access, topology, routing | `apsis-constellation`, `apsis-coverage`, `apsis-access`, `apsis-topology`, `apsis-isl`, `apsis-routing`, `apsis-handover`, `apsis-mesh` |
| ops | 资源、任务、调度、冲突、计划 | `apsis-resource`、`apsis-task`、`apsis-scheduler`、`apsis-conflict`、`apsis-schedule` | | ops | Resources, tasks, scheduling, conflicts, schedules | `apsis-resource`, `apsis-task`, `apsis-scheduler`, `apsis-conflict`, `apsis-schedule` |
| sim | 仿真引擎、场景、事件、输出 | `apsis-sim-engine`、`apsis-scenario`、`apsis-event`、`apsis-output`、`apsis-sim` | | sim | Simulation engine, scenarios, events, output | `apsis-sim-engine`, `apsis-scenario`, `apsis-event`, `apsis-output`, `apsis-sim` |
| facade | 对外门面 | `apsis-rs` | | facade | Public facade | `apsis-rs` |
## 构建 ## Building
所有 Rust 命令在仓库根目录执行: Run every Rust command from the repository root:
```bash ```bash
cargo check --workspace cargo check --workspace
cargo test -p apsis-core -p apsis-units -p apsis-math cargo test -p apsis-core -p apsis-units -p apsis-math
``` ```
`apsis-rs` 门面通过 feature 选择需要的层,例如: The `apsis-rs` facade selects the layers it needs by feature:
```toml ```toml
[dependencies] [dependencies]
apsis-rs = { path = "...", features = ["orbit"] } apsis-rs = { path = "...", features = ["orbit"] }
``` ```
可选 feature:`foundation`、`physics`、`orbit`、`rf`、`network`、`ops`、`sim`、`full`。 The features are `foundation`, `physics`, `orbit`, `rf`, `network`, `ops`,
每个层 feature 自动启用其下层。`full` 启用全部层(含 `sim`);`ops` 等价于除 `sim` and `full`. A layer feature enables the layers below it. `full` enables
`sim` 以外的所有层,适合不运行仿真的项目。 everything including `sim`; `ops` is every layer except `sim`, for a project
that does not run simulations.
## 约定 ## Conventions
- toolchain:`stable`,`edition 2024`,`rust-version 1.85` - Toolchain `stable`, edition 2024, rust-version 1.85
- 默认禁止任何第三方 crate,包括开发依赖与 `build.rs` - No third-party crate by default, including dev-dependencies and build scripts
- 库代码不使用 `unwrap()` / `expect()` / `panic!` / `unsafe` - Library code uses no `unwrap`, `expect` or `panic`. A failure on external
- 外部输入的失败一律走 `Result` / `Option`,不用 `unwrap` / `expect` 转换 input is always a `Result` or an `Option`. An `expect` is allowed on an
- 允许对**已守卫的内部不变量**用 `expect("…")` 断言不可能的分支(如刚 `push` internal invariant that a check just above already guards, such as the
后的 `last()`、前置 `is_none()` 已检查后的取值)。这些断言不处理不可信 element just pushed onto a vector, or a value whose absence the line above
输入,而是把"若此处为空则是编码错误"写成可读的失败信息;全库共 12 处, ruled out. There are twelve such sites and each is listed with its guard in
逐处均有前置守卫。 `docs/design/errors.md`.
- 公共 API 必须有文档注释,注释自包含 - Every public item carries a documentation comment, and the comment is
self-contained
- Tests live in each crate's `tests/` directory, separate from `src/`. They are
integration tests and reach the crate only through its public API; no inline
`#[cfg(test)]` module is used
- Documentation follows the `X.md` and `X.zh.md` convention, with English in
`X.md` and Simplified Chinese in `X.zh.md`
## 目录 ## Layout
```text ```text
apsis-rs/ apsis-rs/
├── Cargo.toml ├── Cargo.toml
├── crates/ # Rust 工作区成员 ├── crates/ # Rust workspace members
│ └── apsis-rs/ │ └── apsis-rs/
│ ├── examples/ # 对拍导出与跨层集成检查 │ ├── examples/ # validation dumps and cross-layer checks
│ └── ... │ └── ...
├── scripts/ # 仓库检查脚本 ├── scripts/ # repository check scripts
├── validation/ # Python 对拍验证 ├── validation/ # Python cross-checks
└── docs/design/ # 设计文档 └── docs/design/ # design notes
``` ```
测试在各 crate 的 `tests/` 目录,为集成测试,只通过公共 API 访问被测 crate。 Tests live in each crate's `tests/` directory. The cross-layer integration
跨层集成检查与零依赖基准测试是 `apsis-rs` 的 example: checks and the dependency-free benchmarks are examples of the facade:
```bash ```bash
# 跨层集成检查(CI 会执行) # Cross-layer integration checks, which CI runs
cargo run -p apsis-rs --features network --example integrate_network cargo run -p apsis-rs --features network --example integrate_network
cargo run -p apsis-rs --features ops --example integrate_ops cargo run -p apsis-rs --features ops --example integrate_ops
cargo run -p apsis-rs --features sim --example integrate_sim cargo run -p apsis-rs --features sim --example integrate_sim
# 热点路径基准(release 下运行) # Hot path benchmark, run in release
cargo run --release -p apsis-rs --features full --example bench cargo run --release -p apsis-rs --features full --example bench
# 规模扫描:输入规模递增,观察每单位工作量的增长趋势 # Scale sweep: the input grows and the cost per unit of work shows the trend
cargo run --release -p apsis-rs --features full --example bench_scale cargo run --release -p apsis-rs --features full --example bench_scale
# 并行扫描:同一批工作量在 1/2/4/8/16 线程下的加速比与效率 # Parallel sweep: the same work over 1, 2, 4, 8 and 16 threads
cargo run --release -p apsis-rs --features full --example bench_parallel cargo run --release -p apsis-rs --features full --example bench_parallel
``` ```
## 许可 ## Documentation
MIT OR Apache-2.0,见 `LICENSE-MIT` 与 `LICENSE-APACHE`。 The design notes record the cross-crate decisions that a reader of a single
crate cannot infer:
- [Units and coordinates](docs/design/units.md)
- [Errors and tolerances](docs/design/errors.md)
- [Layer boundaries](docs/design/layers.md)
- [Parallelism](docs/design/parallel.md)
- [The simulation layer](docs/design/sim.md)
## Licence
MIT OR Apache-2.0, see `LICENSE-MIT` and `LICENSE-APACHE`.

112
README.zh.md Normal file
View File

@ -0,0 +1,112 @@
# ApsisRS / 拱点
商业航天与卫星互联网领域的 Rust 计算内核。
[English](./README.md) | 简体中文
## 定位
- 纯 Rust、`std`、零第三方 crate 依赖
- 领域边界清晰,按层单向依赖
- 对外门面 crate 名为 `apsis-rs`,库名为 `apsis`
- 当前阶段所有 crate 均 `publish = false`,不执行发布
## 分层
```text
foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs
```
约束:
- 依赖只能从左向右。下层可依赖其上任意层,上层不得依赖其下任意层。
例如 `network` 可以依赖 `orbit`(向左),但 `orbit` 不得依赖 `network`
(向右,即反向)。左向依赖不限层数,允许跨层,因为把 `orbit` 的值
传到 `network` 时中间层未必提供中转类型。
- `foundation` 的 `apsis-core` 是全局基准,任何层任何 crate 均可依赖它。
`apsis-math` 同理,因为线性代数被各层共用。
- 层内以该层的门面 crate(`apsis-orbit`、`apsis-rf`、`apsis-sim`)作为整合点。
这套规则由 `scripts/check-layers.sh` 强制,CI 会运行它。
各层内容:
| 层 | 职责 | crates |
| --- | --- | --- |
| foundation | 基础类型、单位、数学、宏 | `apsis-core`、`apsis-units`、`apsis-math`、`apsis-macros` |
| physics | 时间、闰秒、坐标、大地基准 | `apsis-time`、`apsis-leap`、`apsis-coord`、`apsis-geodesy`、`apsis-frames` |
| orbit | TLE、SGP4、二体、星历、机动 | `apsis-tle`、`apsis-sgp4`、`apsis-kepler`、`apsis-ephemeris`、`apsis-maneuver`、`apsis-orbit` |
| rf | 射频、天线、链路预算 | `apsis-rf`、`apsis-antenna`、`apsis-doppler`、`apsis-atmosphere`、`apsis-noise`、`apsis-interference`、`apsis-link` |
| network | 星座、覆盖、接入、拓扑、路由 | `apsis-constellation`、`apsis-coverage`、`apsis-access`、`apsis-topology`、`apsis-isl`、`apsis-routing`、`apsis-handover`、`apsis-mesh` |
| ops | 资源、任务、调度、冲突、计划 | `apsis-resource`、`apsis-task`、`apsis-scheduler`、`apsis-conflict`、`apsis-schedule` |
| sim | 仿真引擎、场景、事件、输出 | `apsis-sim-engine`、`apsis-scenario`、`apsis-event`、`apsis-output`、`apsis-sim` |
| facade | 对外门面 | `apsis-rs` |
## 构建
所有 Rust 命令在仓库根目录执行:
```bash
cargo check --workspace
cargo test -p apsis-core -p apsis-units -p apsis-math
```
`apsis-rs` 门面通过 feature 选择需要的层,例如:
```toml
[dependencies]
apsis-rs = { path = "...", features = ["orbit"] }
```
可选 feature:`foundation`、`physics`、`orbit`、`rf`、`network`、`ops`、`sim`、`full`。
每个层 feature 自动启用其下层。`full` 启用全部层(含 `sim`);`ops` 等价于除
`sim` 以外的所有层,适合不运行仿真的项目。
## 约定
- toolchain:`stable`,`edition 2024`,`rust-version 1.85`
- 默认禁止任何第三方 crate,包括开发依赖与 `build.rs`
- 库代码不使用 `unwrap()` / `expect()` / `panic!` / `unsafe`
- 外部输入的失败一律走 `Result` / `Option`,不用 `unwrap` / `expect` 转换
- 允许对**已守卫的内部不变量**用 `expect("…")` 断言不可能的分支(如刚 `push`
后的 `last()`、前置 `is_none()` 已检查后的取值)。这些断言不处理不可信
输入,而是把"若此处为空则是编码错误"写成可读的失败信息;全库共 12 处,
逐处均有前置守卫。
- 公共 API 必须有文档注释,注释自包含
## 目录
```text
apsis-rs/
├── Cargo.toml
├── crates/ # Rust 工作区成员
│ └── apsis-rs/
│ ├── examples/ # 对拍导出与跨层集成检查
│ └── ...
├── scripts/ # 仓库检查脚本
├── validation/ # Python 对拍验证
└── docs/design/ # 设计文档
```
测试在各 crate 的 `tests/` 目录,为集成测试,只通过公共 API 访问被测 crate。
跨层集成检查与零依赖基准测试是 `apsis-rs` 的 example:
```bash
# 跨层集成检查(CI 会执行)
cargo run -p apsis-rs --features network --example integrate_network
cargo run -p apsis-rs --features ops --example integrate_ops
cargo run -p apsis-rs --features sim --example integrate_sim
# 热点路径基准(release 下运行)
cargo run --release -p apsis-rs --features full --example bench
# 规模扫描:输入规模递增,观察每单位工作量的增长趋势
cargo run --release -p apsis-rs --features full --example bench_scale
# 并行扫描:同一批工作量在 1/2/4/8/16 线程下的加速比与效率
cargo run --release -p apsis-rs --features full --example bench_parallel
```
## 许可
MIT OR Apache-2.0,见 `LICENSE-MIT` 与 `LICENSE-APACHE`。

View File

@ -1,17 +1,22 @@
# 设计文档 # Design notes
本目录记录 ApsisRS 的跨 crate 设计决策,例如坐标约定、单位约定、误差策略与 English | [简体中文](./README.zh.md)
分层边界。单个 crate 的内部设计写在对应 crate 的模块文档中。
文档内容需自包含,仅引用本仓库内的代码或可公开访问的标准。 This directory records the cross-crate design decisions of ApsisRS, such as
the coordinate and unit conventions, the error and tolerance strategy, the
layer boundaries and the parallelism. The design inside a single crate is
documented in that crate's module documentation.
## 跨层约定 A note is self-contained and refers only to code in this repository or to a
public standard.
- [单位与坐标约定](units.md) ## Cross-layer conventions
- [错误与容差策略](errors.md)
- [分层边界](layers.md)
- [并行化](parallel.md)
## 各层设计 - [Units and coordinates](units.md)
- [Errors and tolerances](errors.md)
- [Layer boundaries](layers.md)
- [Parallelism](parallel.md)
- [仿真层](sim.md) ## Per-layer design
- [The simulation layer](sim.md)

19
docs/design/README.zh.md Normal file
View File

@ -0,0 +1,19 @@
# 设计文档
[English](./README.md) | 简体中文
本目录记录 ApsisRS 的跨 crate 设计决策,例如坐标约定、单位约定、误差策略与
分层边界。单个 crate 的内部设计写在对应 crate 的模块文档中。
文档内容需自包含,仅引用本仓库内的代码或可公开访问的标准。
## 跨层约定
- [单位与坐标约定](units.md)
- [错误与容差策略](errors.md)
- [分层边界](layers.md)
- [并行化](parallel.md)
## 各层设计
- [仿真层](sim.md)

View File

@ -1,19 +1,23 @@
# 错误与容差策略 # Errors and tolerances
本文件记录跨 crate 的错误表达与数值容差约定。单个 crate 的错误枚举见其 English | [简体中文](./errors.zh.md)
`error.rs` 的文档。
## 错误表达 This file records how errors are expressed across crates and how numerical
tolerances are chosen. The error enumeration of a single crate is documented
in its `error.rs`.
### 领域错误 ## Expressing an error
每个有领域特定失败的 crate 定义**自己的**错误类型,命名与结构一致: ### A domain error
Every crate with a domain specific failure defines **its own** error type,
named and shaped consistently:
```rust ```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive] #[non_exhaustive]
pub enum FooErrorKind { pub enum FooErrorKind {
/// 每个变体描述一个调用方可能想要区分的具体条件。 /// Each variant names a concrete condition a caller may want to tell apart.
ReversedInterval, ReversedInterval,
DuplicateResource, DuplicateResource,
// ... // ...
@ -32,119 +36,148 @@ impl FooError {
pub fn detail(&self) -> Option<&str>; pub fn detail(&self) -> Option<&str>;
} }
impl fmt::Display for FooError { /* "kind" 或 "kind: detail" */ } impl fmt::Display for FooError { /* "kind", or "kind: detail" */ }
impl core::error::Error for FooError {} impl core::error::Error for FooError {}
``` ```
约定: The conventions:
- **`ErrorKind` 标注 `#[non_exhaustive]`**,这样新增一个失败类别不会破坏 - **An `ErrorKind` is marked `#[non_exhaustive]`**, so that adding a failure
下游的匹配。当前 15 个 `ErrorKind` 全部标注。 category does not break a match downstream. All fifteen `ErrorKind`
- **错误枚举本身也标注 `#[non_exhaustive]`**,包括同时携带数据的错误枚举 enumerations carry the attribute.
(如 `apsis-orbit` 的 `EphemerisSamplingError`,其变体包裹下层错误)。 - **An error enum that carries data is marked too**, including one whose
- **非错误的状态枚举不标注。** 领域状态(`TaskState`、`LinkState`、 variants wrap a lower error, such as `apsis-orbit`'s
`TimeScale`、`HandoverDecision`、`Pattern` 的选择等)在单仓库 `EphemerisSamplingError`.
工作区内以**可穷尽匹配为优点**:新增一个变体时,编译器会指出每一处需要 - **A state enum that is not an error is not marked.** A domain state such as
更新的 `match`。这与错误的取舍相反——错误类别多一个,调用方应能继续 `TaskState`, `LinkState`, `TimeScale`, `HandoverDecision` or the antenna
走默认分支;状态多一个,调用方通常必须显式处理。判断标准:该 enum 的 `Pattern` gains from being matchable exhaustively inside a single
变体是否会被调用方 `match` 后对某些分支走默认?会,则标;不会,则不标。 repository: when a variant is added, the compiler points at every `match`
- **`kind` 可精确匹配,`detail` 供人阅读。** 调用方按 `kind` 写恢复逻辑, that needs updating. The reasoning is the opposite of an error's, because
把 `detail` 记入日志。`detail` 不参与匹配,因此其措辞可自由改动。 one more error category should let a caller fall through to a default
- **`Display` 输出 `kind`,有 `detail` 时追加 `": detail"`。** 这样日志行 branch, while one more state usually has to be handled explicitly. The test
总是以类别开头。 is whether a caller will `match` the enum and take a default on some
- **实现 `core::error::Error`**,以便错误可以嵌套为 `source`。当前 15 个 branches: if so, mark it; if not, leave it.
错误类型全部实现。 - **`kind` is matched, `detail` is read.** A caller matches on `kind` to
- **不返回裸 `String` 或 `Box<dyn Error>` 作为公共错误。** 前者无法匹配, write recovery logic and logs `detail`. Because `detail` takes no part in
后者丢失类别。私有辅助函数内部可以传 `String` 作为原因文本,但公开 matching, its wording may change freely.
边界必须是枚举。`apsis-scheduler` 的 `precedence_floor` 是这种内部用法 - **`Display` prints the kind**, followed by `": detail"` when there is one,
的实例:它私有的 `String` 最终填入公开的 `TaskState::Infeasible { reason }`。 so a log line always begins with the category.
- **`core::error::Error` is implemented**, so an error can nest as the
`source` of another. All fifteen error types implement it.
- **A bare `String` or `Box<dyn Error>` is never a public error.** The first
cannot be matched and the second loses the category. A private helper may
pass a `String` as a reason internally, but the public boundary is an enum.
The `precedence_floor` in `apsis-scheduler` is an instance: its private
`String` ends up inside the public `TaskState::Infeasible { reason }`.
### 通用错误 ### The general error
`apsis-core::Error` 与 `ErrorKind` 是通用兜底类型,携带粗粒度类别 `apsis-core::Error` and `ErrorKind` are a general purpose fallback carrying
(`InvalidInput`、`NotFound` 等)。它**不是**各层的统一错误表面:没有任何 a coarse category such as `InvalidInput` or `NotFound`. They are **not** the
层使用它,因为通用类别丢失了调用方需要的区分(`InvalidInput` 无法区分 error surface of the layers: no layer uses them, because a coarse category
"区间反向"与"重复标识")。 loses the distinction a caller needs. `InvalidInput` cannot tell a reversed
interval from a duplicate identifier.
它保留在这里有两个理由:给一个失败没有有趣结构的函数一个现成的返回类型; They are kept for two reasons: to give a function whose failure has no
以及给工作区一个可扩展的兜底形状。新代码若失败有领域含义,应定义领域错误。 interesting structure a ready return type, and to give the workspace one
extensible fallback shape. New code whose failure has a domain meaning should
define a domain error.
### 失败与"无值"的区分 ### Failure against the absence of a value
- **`Option`** 表示"没有值"是正常结果,不是错误。例如某个时刻在覆盖区间 - **`Option`** means that having no value is an ordinary outcome rather than
之外(`Ephemeris::state_at` 返回 `None`)、某个角度的正切无定义。 a failure, such as an instant outside the covered interval, which
- **`Result`** 表示操作因可诊断的原因失败。例如区间反向、资源重复、 `Ephemeris::state_at` reports as `None`, or a tangent that is undefined at
超出模型定义域。 an angle.
- **`Result`** means an operation failed for a diagnosable reason, such as a
reversed interval, a duplicate resource, or a value outside the domain the
model covers.
判断标准:调用方是否需要知道**为什么**。若只需要知道"没有",用 `Option`; The test is whether the caller needs to know **why**. If it only needs to
若需要区分失败原因并据此恢复,用 `Result`。 know that there is nothing, use `Option`; if it needs to tell the failures
apart and recover, use `Result`.
## 数值容差 ## Numerical tolerances
### 迭代收敛容差 ### Convergence of an iteration
迭代求解(开普勒方程、章动级数、射线追踪)用 crate 内的私有常量控制收敛: An iterative solve, whether of Kepler's equation, a nutation series or a ray
trace, is governed by a private constant inside its crate:
| crate | 常量 | 值 | 含义 | | Crate | Constant | Value | Meaning |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `apsis-kepler` | `TOLERANCE` | `1e-14` | 偏近点角牛顿迭代的步长 | | `apsis-kepler` | `TOLERANCE` | `1e-14` | the step of the Newton iteration for the eccentric anomaly |
| `apsis-sgp4` | `KEPLER_TOLERANCE` | `1e-12` | 开普勒方程迭代残差 | | `apsis-sgp4` | `KEPLER_TOLERANCE` | `1e-12` | the residual of the Kepler equation iteration |
这些值在浮点精度与迭代次数之间折中:取到接近 `f64` 的机器精度,同时 These values trade floating point precision against iteration count: they sit
保证迭代在有限步内收敛(各有对应的 `MAX_ITERATIONS`)。 close to the machine precision of an `f64` while still guaranteeing that the
iteration terminates, each with its own `MAX_ITERATIONS` bound.
### 对拍容差 ### Tolerances of the cross-checks
对拍脚本的容差按**物理量**设定,而不是统一的数值: A cross-check sets its tolerance by the **physical quantity** rather than by
one number for everything:
| 量 | 容差 | | Quantity | Tolerance |
| --- | --- | | --- | --- |
| 位置 | < 1e-3 km | | Position | < 1e-3 km |
| 速度 | < 1e-6 km/s | | Velocity | < 1e-6 km/s |
| 时间 | < 1e-6 s | | Time | < 1e-6 s |
| 覆盖几何角度 | 1e-12 rad | | Coverage angle | 1e-12 rad |
| 覆盖面积、占比 | 相对 1e-12 | | Coverage area and fraction | 1e-12 relative |
容差设在"远高于观测到的偏差(通常在浮点精度量级),远低于模型本身的 A tolerance sits far above the disagreement actually observed, which is
精度"之间。这样既能捕获半径写错、单位漏换算这类错误,又不会因最后一位 usually at the floating point level, and far below the accuracy of the model
浮点差异而误报。详见 `validation/README.md` 的容差表。 itself. That way it catches a mistyped radius or a missing unit conversion
without reporting on the last bit of a floating point value. The table in
`validation/README.zh.md` lists them per script.
### 精确比较 ### Comparing exactly
不需要容差时应避免引入:整数索引、计数、布尔标志用精确相等;角度归约 A tolerance should be avoided where it is not needed. An integer index, a
(`wrap_to_pi`)在边界上用半开区间,不引入 epsilon。浮点比较仅在量本身 count or a flag is compared for equality. An angle reduction such as
是连续近似时使用。 `wrap_to_pi` uses a half-open interval at the boundary rather than an
epsilon. A floating point comparison is for a quantity that is genuinely a
continuous approximation.
## 库代码中的 expect 断言 ## expect in library code
工作区约定库代码不使用 `unwrap` / `expect` / `panic!`。这条规则的**意图**是 The workspace convention is that library code uses no `unwrap`, `expect` or
不允许用它们处理外部输入的失败——那种失败必须走 `Result` 或 `Option`,让 `panic!`. The **intent** of the rule is that they must not handle a failure
调用方能够处理。 on external input, which has to become a `Result` or an `Option` the caller
can act on.
但完全不出现 `expect` 会迫使代码用 `unwrap`(更差)或写出冗长的手工分支。 Allowing no `expect` at all would push the code towards `unwrap`, which is
因此允许一种例外:**对已由前置检查守卫的内部不变量使用 worse, or into long hand written branches. So one case is permitted:
`expect("说明")`**。这类断言不把不可信数据转成 panic,而是把"若此处为空, **an `expect` on an internal invariant that a check has already guarded.**
则是本 crate 的编码错误"写成一条可读的失败信息。 Such an assertion does not turn untrusted data into a panic; it turns "if
this were empty it would be a bug in this crate" into a readable failure
message.
判据:能否在紧邻的前一行找到使该分支不可达的守卫,或该值是否由本函数 The test is whether a guard making the branch unreachable sits on the line
刚刚构造。当前 12 处全部满足,逐处为: above, or whether the value was just built by this function. All thirteen
current sites satisfy it:
| 位置 | 不变量 | 守卫 | | Site | Invariant | Guard |
| --- | --- | --- | | --- | --- | --- |
| `apsis-schedule/schedule.rs` | 刚 `push` 的 placement 存在 | 同函数上一行 | | `apsis-math/src/parallel.rs` | every slot was filled by one thread | the loop assigns each index exactly once |
| `apsis-scheduler/improve.rs` | 刚检查 `is_none()` 的值非空 | 上一行 `is_none()` 分支 | | `apsis-schedule/src/schedule.rs` | the placement just pushed exists | the line above |
| `apsis-scheduler/planner.rs` | order 中的 id 在 task 列表里 | order 由 task 列表构造 | | `apsis-scheduler/src/improve.rs` | the value whose `is_none()` was just checked is not none | the `is_none()` branch above |
| `apsis-topology/series.rs` | 时间有限 | 构造时已校验 | | `apsis-scheduler/src/planner.rs` | an identifier in the order is in the task list | the order is built from the task list |
| `apsis-handover/decision.rs` | 候选列表非空 | 上一行 `validate` | | `apsis-topology/src/series.rs` | the time is finite | validated on construction |
| `apsis-constellation/walker.rs` | 平面数 / 每面星数为正 | 构造时已校验除数 | | `apsis-handover/src/decision.rs` | the candidate list is not empty | the `validate` call above |
| `apsis-kepler/elements.rs` | node / 半径非零 | 紧邻的 `norm() == 0.0` 分支 | | `apsis-constellation/src/walker.rs` | the plane count and the satellites per plane are positive | the divisor is validated before |
| `apsis-event/queue.rs` | 出堆项必有 payload | 队列构造时序保证 | | `apsis-kepler/src/elements.rs` | the node, the radius and the angular momentum are non-zero | the `norm() == 0.0` branch just above |
| `apsis-event/src/queue.rs` | a popped entry has its payload | guaranteed by the construction order of the queue |
若将某条守卫移除,相应的 `expect` 就必须改为 `Result`。修改时二者应同时进行。 Removing a guard means the matching `expect` must become a `Result`; the two
change together.
## 检查 ## Checks
- 各 crate 测试覆盖其错误的每个 `kind`,包括构造失败与恢复路径。 - Each crate's tests cover every `kind` of its error, including the path
- 对拍脚本以退出码表达结果:0 通过、1 超差、2 缺输入。 that constructs it and the path that recovers from it.
- `cargo clippy -- -D warnings` 保证没有 `unwrap`/`expect` 漏进库代码 - A cross-check reports through its exit code: zero to pass, one to exceed
(工作区约定库代码不使用它们)。 the tolerance, two for a missing input.
- `cargo clippy -- -D warnings` keeps an `unwrap` or `expect` from reaching
library code unnoticed.

153
docs/design/errors.zh.md Normal file
View File

@ -0,0 +1,153 @@
# 错误与容差策略
[English](./errors.md) | 简体中文
本文件记录跨 crate 的错误表达与数值容差约定。单个 crate 的错误枚举见其
`error.rs` 的文档。
## 错误表达
### 领域错误
每个有领域特定失败的 crate 定义**自己的**错误类型,命名与结构一致:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FooErrorKind {
/// 每个变体描述一个调用方可能想要区分的具体条件。
ReversedInterval,
DuplicateResource,
// ...
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FooError {
kind: FooErrorKind,
detail: Option<String>,
}
impl FooError {
pub fn new(kind: FooErrorKind) -> Self;
pub fn with_detail(self, detail: impl Into<String>) -> Self;
pub fn kind(&self) -> FooErrorKind;
pub fn detail(&self) -> Option<&str>;
}
impl fmt::Display for FooError { /* "kind" 或 "kind: detail" */ }
impl core::error::Error for FooError {}
```
约定:
- **`ErrorKind` 标注 `#[non_exhaustive]`**,这样新增一个失败类别不会破坏
下游的匹配。当前 15 个 `ErrorKind` 全部标注。
- **错误枚举本身也标注 `#[non_exhaustive]`**,包括同时携带数据的错误枚举
(如 `apsis-orbit` 的 `EphemerisSamplingError`,其变体包裹下层错误)。
- **非错误的状态枚举不标注。** 领域状态(`TaskState`、`LinkState`、
`TimeScale`、`HandoverDecision`、`Pattern` 的选择等)在单仓库
工作区内以**可穷尽匹配为优点**:新增一个变体时,编译器会指出每一处需要
更新的 `match`。这与错误的取舍相反——错误类别多一个,调用方应能继续
走默认分支;状态多一个,调用方通常必须显式处理。判断标准:该 enum 的
变体是否会被调用方 `match` 后对某些分支走默认?会,则标;不会,则不标。
- **`kind` 可精确匹配,`detail` 供人阅读。** 调用方按 `kind` 写恢复逻辑,
把 `detail` 记入日志。`detail` 不参与匹配,因此其措辞可自由改动。
- **`Display` 输出 `kind`,有 `detail` 时追加 `": detail"`。** 这样日志行
总是以类别开头。
- **实现 `core::error::Error`**,以便错误可以嵌套为 `source`。当前 15 个
错误类型全部实现。
- **不返回裸 `String` 或 `Box<dyn Error>` 作为公共错误。** 前者无法匹配,
后者丢失类别。私有辅助函数内部可以传 `String` 作为原因文本,但公开
边界必须是枚举。`apsis-scheduler` 的 `precedence_floor` 是这种内部用法
的实例:它私有的 `String` 最终填入公开的 `TaskState::Infeasible { reason }`。
### 通用错误
`apsis-core::Error` 与 `ErrorKind` 是通用兜底类型,携带粗粒度类别
(`InvalidInput`、`NotFound` 等)。它**不是**各层的统一错误表面:没有任何
层使用它,因为通用类别丢失了调用方需要的区分(`InvalidInput` 无法区分
"区间反向"与"重复标识")。
它保留在这里有两个理由:给一个失败没有有趣结构的函数一个现成的返回类型;
以及给工作区一个可扩展的兜底形状。新代码若失败有领域含义,应定义领域错误。
### 失败与"无值"的区分
- **`Option`** 表示"没有值"是正常结果,不是错误。例如某个时刻在覆盖区间
之外(`Ephemeris::state_at` 返回 `None`)、某个角度的正切无定义。
- **`Result`** 表示操作因可诊断的原因失败。例如区间反向、资源重复、
超出模型定义域。
判断标准:调用方是否需要知道**为什么**。若只需要知道"没有",用 `Option`;
若需要区分失败原因并据此恢复,用 `Result`。
## 数值容差
### 迭代收敛容差
迭代求解(开普勒方程、章动级数、射线追踪)用 crate 内的私有常量控制收敛:
| crate | 常量 | 值 | 含义 |
| --- | --- | --- | --- |
| `apsis-kepler` | `TOLERANCE` | `1e-14` | 偏近点角牛顿迭代的步长 |
| `apsis-sgp4` | `KEPLER_TOLERANCE` | `1e-12` | 开普勒方程迭代残差 |
这些值在浮点精度与迭代次数之间折中:取到接近 `f64` 的机器精度,同时
保证迭代在有限步内收敛(各有对应的 `MAX_ITERATIONS`)。
### 对拍容差
对拍脚本的容差按**物理量**设定,而不是统一的数值:
| 量 | 容差 |
| --- | --- |
| 位置 | < 1e-3 km |
| 速度 | < 1e-6 km/s |
| 时间 | < 1e-6 s |
| 覆盖几何角度 | 1e-12 rad |
| 覆盖面积、占比 | 相对 1e-12 |
容差设在"远高于观测到的偏差(通常在浮点精度量级),远低于模型本身的
精度"之间。这样既能捕获半径写错、单位漏换算这类错误,又不会因最后一位
浮点差异而误报。详见 `validation/README.zh.md` 的容差表。
### 精确比较
不需要容差时应避免引入:整数索引、计数、布尔标志用精确相等;角度归约
(`wrap_to_pi`)在边界上用半开区间,不引入 epsilon。浮点比较仅在量本身
是连续近似时使用。
## 库代码中的 expect 断言
工作区约定库代码不使用 `unwrap` / `expect` / `panic!`。这条规则的**意图**是
不允许用它们处理外部输入的失败——那种失败必须走 `Result` 或 `Option`,让
调用方能够处理。
但完全不出现 `expect` 会迫使代码用 `unwrap`(更差)或写出冗长的手工分支。
因此允许一种例外:**对已由前置检查守卫的内部不变量使用
`expect("说明")`**。这类断言不把不可信数据转成 panic,而是把"若此处为空,
则是本 crate 的编码错误"写成一条可读的失败信息。
判据:能否在紧邻的前一行找到使该分支不可达的守卫,或该值是否由本函数
刚刚构造。当前 13 处全部满足,逐处为:
| 位置 | 不变量 | 守卫 |
| --- | --- | --- |
| `apsis-math/src/parallel.rs` | 每个槽位都被唯一线程填过 | 循环为每个下标恰好赋值一次 |
| `apsis-schedule/src/schedule.rs` | 刚 `push` 的 placement 存在 | 同函数上一行 |
| `apsis-scheduler/src/improve.rs` | 刚检查 `is_none()` 的值非空 | 上一行 `is_none()` 分支 |
| `apsis-scheduler/src/planner.rs` | order 中的 id 在 task 列表里 | order 由 task 列表构造 |
| `apsis-topology/src/series.rs` | 时间有限 | 构造时已校验 |
| `apsis-handover/src/decision.rs` | 候选列表非空 | 上一行 `validate` |
| `apsis-constellation/src/walker.rs` | 平面数 / 每面星数为正 | 构造时已校验除数 |
| `apsis-kepler/src/elements.rs` | node / 半径 / 角动量非零 | 紧邻的 `norm() == 0.0` 分支 |
| `apsis-event/src/queue.rs` | 出堆项必有 payload | 队列构造时序保证 |
若将某条守卫移除,相应的 `expect` 就必须改为 `Result`。修改时二者应同时进行。
## 检查
- 各 crate 测试覆盖其错误的每个 `kind`,包括构造失败与恢复路径。
- 对拍脚本以退出码表达结果:0 通过、1 超差、2 缺输入。
- `cargo clippy -- -D warnings` 保证没有 `unwrap`/`expect` 漏进库代码
(工作区约定库代码不使用它们)。

View File

@ -1,89 +1,107 @@
# 分层边界 # Layer boundaries
本文件记录工作区的分层规则,以及为什么某些跨层依赖是允许的。README 的 English | [简体中文](./layers.zh.md)
"分层"一节是规则本身,这里是它的判定标准与边界情况。
## 规则 This file records the layering rule of the workspace and why certain
cross-layer dependencies are allowed. The "Layers" section of the README is
the rule itself; what belongs here is how it is decided and where it has edge
cases.
层自左向右排列: ## The rule
The layers run left to right:
```text ```text
foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs
``` ```
一条依赖边从 crate A 指向 crate B。把 A 所在的层与 B 所在的层分别映射为 A dependency edge runs from crate A to crate B. Map the layer of A and the
下标,规则是: layer of B to indices, and the rule is:
```text ```text
index(B) <= index(A) 允许 index(B) <= index(A) allowed
index(B) > index(A) 违规(反向依赖) index(B) > index(A) a violation, a reverse dependency
``` ```
即**只能向左依赖,不能向右依赖**。`scripts/check-layers.sh` 对工作区每个 In other words, **dependencies go to the left and never to the right.**
清单执行这条判定,CI 会运行它。 `scripts/check-layers.sh` applies this to every manifest in the workspace,
and CI runs it.
## 允许跨层 ## Crossing layers is allowed
规则只看方向,不看距离。从 `network` 直接依赖 `orbit` 是允许的,尽管中间 The rule constrains direction, not distance. `network` may depend on `orbit`
隔着 `rf`:`orbit` 在 `network` 左边,方向正确。 even though `rf` sits between them, because `orbit` is to the left of
`network` and the direction is correct.
允许向左跨层是有意的。若强制只能依赖紧邻的左层,会产生两类问题: Allowing a leftward crossing is deliberate. Forbidding it would create two
kinds of problem:
- **中转类型不存在。** 把 `orbit` 层的位置传给 `network` 层时,`rf` 层并 - **There may be no type to carry the value.** When a position from the
非必然提供一个承载该位置的类型。强行要求经过 `rf` 只会制造只是转发的 `orbit` layer is handed to the `network` layer, the `rf` layer need not
空壳类型。 offer a type that carries it. Requiring the trip through `rf` would only
- **无谓的耦合。** `network` 的星座计算需要 `apsis-kepler` 的开普勒要素, produce a shell type that forwards.
它并不需要 `rf`。若禁止跨层,`network` 就得依赖 `rf` 只为拿到一个它不 - **It would be coupling without need.** The constellation calculation in
使用的类型。 `network` wants the Keplerian elements from `apsis-kepler` and has no use
for `rf`. Forbidding the crossing would make `network` depend on `rf` only
to reach a type it does not use.
因此规则约束的是**方向**,因为方向才对应真正的分层语义:上层的概念可以 The rule therefore constrains **direction**, because direction is what
引用下层的概念,反之则说明分层错了。 carries the meaning of layering: a concept of a higher layer may refer to a
concept of a lower one, and the other way round would mean the layering is
wrong.
## 全局基准 ## The global baselines
`apsis-core`(通用错误、常量、单位标记)与 `apsis-math`(`Vec3`、 `apsis-core` (the general error, the constants, the unit marker) and
`Mat3`、`Quat`)被各层共用。它们本身在 `foundation` 中,所以"只向左依赖" `apsis-math` (`Vec3`, `Mat3`, `Quat`) are shared by every layer. They sit in
已经允许任何层使用它们,无需特例。 `foundation` themselves, so "to the left only" already lets any layer use
them and no special case is needed.
这与"层内基准"不同:`apsis-core` 也是 `foundation` 内的基准,但它的可用 This differs from a within-layer baseline. `apsis-core` is also the baseline
范围是整个工作区,而不只是同层。 inside `foundation`, but its reach is the whole workspace rather than one
layer.
**只有真正使用的依赖才声明。** `apsis-core` 与 `apsis-math` 可以被任何 **Declare a dependency only where it is used.** Any crate may use
crate 使用,但一个不引用它们的 crate 不应把它们写进清单:未使用的依赖会 `apsis-core` and `apsis-math`, but a crate that does not reference them
拖慢编译、让 `cargo tree` 与分层检查产生噪声,并让"谁依赖什么"失真。 should not list them: an unused dependency slows the build, adds noise to
需要时再加,是一条随时可走的单行改动。`scripts/check-layers.sh` 报告的 `cargo tree` and to the layer check, and misstates who depends on what.
边数因此是真实的使用关系,而不是一份预留清单。 Adding one later is a single line whenever it is needed. The edge count that
`scripts/check-layers.sh` reports is therefore a real usage relation rather
than a list of intentions.
## 层内门面 ## The facade within a layer
每层以一个门面 crate 作为整合点: Each layer has one facade crate as its integration point:
| 层 | 门面 | | Layer | Facade |
| --- | --- | | --- | --- |
| orbit | `apsis-orbit` | | orbit | `apsis-orbit` |
| rf | `apsis-rf` | | rf | `apsis-rf` |
| sim | `apsis-sim` | | sim | `apsis-sim` |
| 对外 | `apsis-rs` | | outward | `apsis-rs` |
门面把同层的能力组合成少量类型,并再导出同层的其余 crate。`foundation`、 A facade combines the capabilities of its layer into a few types and
`physics`、`network`、`ops` 没有单独门面:它们的 crate 各自独立有用,且 re-exports the remaining crates of the same layer. `foundation`, `physics`,
`apsis-rs` 会把它们一并再导出。 `network` and `ops` have no separate facade: their crates are each useful on
their own, and `apsis-rs` re-exports them together.
## 边界情况 ## Edge cases
- **facade `apsis-rs` 不在层序列中。** 它依赖全部层,因为它就是出口。 - **The `apsis-rs` facade is not in the layer sequence.** It depends on
`scripts/check-layers.sh` 通过层目录名判定,`crates/apsis-rs` 不是 every layer, because it is the exit. `scripts/check-layers.sh` decides by
`crates/<layer>/<crate>` 形状,因此被跳过。 the layer directory name, and `crates/apsis-rs` does not have the shape
- **`dev-dependencies` 也计入检查。** 一条测试里的反向依赖同样说明分层 `crates/<layer>/<crate>`, so it is skipped.
错了,应当修正代码而不是放宽测试。 - **Dev-dependencies count.** A reverse dependency in a test shows the
- **门面再导出不构成新依赖。** `apsis-rs` 把 `apsis-orbit` 再导出,是 layering is just as wrong there, so the fix is to change the code rather
门面行为,不是某层对 `orbit` 的依赖。 than relax the test.
- **A facade re-export is not a new dependency.** `apsis-rs` re-exporting
`apsis-orbit` is what a facade does, not a layer reaching into `orbit`.
## 检查 ## Checks
```bash ```bash
bash scripts/check-layers.sh bash scripts/check-layers.sh
``` ```
脚本输出检查过的依赖边数,并在发现反向依赖时逐条列出后以退出码 1 结束。 The script prints the number of dependency edges it checked, and lists each
对当前工作区,它检查 104 条边,全部方向正确。 reverse dependency before exiting with status one if it found any.

91
docs/design/layers.zh.md Normal file
View File

@ -0,0 +1,91 @@
# 分层边界
[English](./layers.md) | 简体中文
本文件记录工作区的分层规则,以及为什么某些跨层依赖是允许的。README 的
"分层"一节是规则本身,这里是它的判定标准与边界情况。
## 规则
层自左向右排列:
```text
foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs
```
一条依赖边从 crate A 指向 crate B。把 A 所在的层与 B 所在的层分别映射为
下标,规则是:
```text
index(B) <= index(A) 允许
index(B) > index(A) 违规(反向依赖)
```
即**只能向左依赖,不能向右依赖**。`scripts/check-layers.sh` 对工作区每个
清单执行这条判定,CI 会运行它。
## 允许跨层
规则只看方向,不看距离。从 `network` 直接依赖 `orbit` 是允许的,尽管中间
隔着 `rf`:`orbit` 在 `network` 左边,方向正确。
允许向左跨层是有意的。若强制只能依赖紧邻的左层,会产生两类问题:
- **中转类型不存在。** 把 `orbit` 层的位置传给 `network` 层时,`rf` 层并
非必然提供一个承载该位置的类型。强行要求经过 `rf` 只会制造只是转发的
空壳类型。
- **无谓的耦合。** `network` 的星座计算需要 `apsis-kepler` 的开普勒要素,
它并不需要 `rf`。若禁止跨层,`network` 就得依赖 `rf` 只为拿到一个它不
使用的类型。
因此规则约束的是**方向**,因为方向才对应真正的分层语义:上层的概念可以
引用下层的概念,反之则说明分层错了。
## 全局基准
`apsis-core`(通用错误、常量、单位标记)与 `apsis-math`(`Vec3`、
`Mat3`、`Quat`)被各层共用。它们本身在 `foundation` 中,所以"只向左依赖"
已经允许任何层使用它们,无需特例。
这与"层内基准"不同:`apsis-core` 也是 `foundation` 内的基准,但它的可用
范围是整个工作区,而不只是同层。
**只有真正使用的依赖才声明。** `apsis-core` 与 `apsis-math` 可以被任何
crate 使用,但一个不引用它们的 crate 不应把它们写进清单:未使用的依赖会
拖慢编译、让 `cargo tree` 与分层检查产生噪声,并让"谁依赖什么"失真。
需要时再加,是一条随时可走的单行改动。`scripts/check-layers.sh` 报告的
边数因此是真实的使用关系,而不是一份预留清单。
## 层内门面
每层以一个门面 crate 作为整合点:
| 层 | 门面 |
| --- | --- |
| orbit | `apsis-orbit` |
| rf | `apsis-rf` |
| sim | `apsis-sim` |
| 对外 | `apsis-rs` |
门面把同层的能力组合成少量类型,并再导出同层的其余 crate。`foundation`、
`physics`、`network`、`ops` 没有单独门面:它们的 crate 各自独立有用,且
`apsis-rs` 会把它们一并再导出。
## 边界情况
- **facade `apsis-rs` 不在层序列中。** 它依赖全部层,因为它就是出口。
`scripts/check-layers.sh` 通过层目录名判定,`crates/apsis-rs` 不是
`crates/<layer>/<crate>` 形状,因此被跳过。
- **`dev-dependencies` 也计入检查。** 一条测试里的反向依赖同样说明分层
错了,应当修正代码而不是放宽测试。
- **门面再导出不构成新依赖。** `apsis-rs` 把 `apsis-orbit` 再导出,是
门面行为,不是某层对 `orbit` 的依赖。
## 检查
```bash
bash scripts/check-layers.sh
```
脚本输出检查过的依赖边数,并在发现反向依赖时逐条列出后以退出码 1 结束。
边数随代码变化,因此此处不写死;运行脚本得到的数字才是当前的真实关系。

View File

@ -1,121 +1,159 @@
# 并行化 # Parallelism
本文件记录并行化的判定标准、提供的接口,以及实测结果。跨层的单位与坐标约定见 English | [简体中文](./parallel.zh.md)
[单位与坐标约定](units.md),分层边界见 [分层边界](layers.md)。
## 为什么不引入线程池 This file records how a loop is judged worth threading, which interfaces the
workspace offers, and what the measurements were. The cross-layer unit and
coordinate conventions are in [Units and coordinates](units.md), and the
layering is in [Layer boundaries](layers.md).
工作区禁止任何第三方 crate,因此没有 `rayon` 这样的线程池可用。也不需要: ## Why there is no thread pool
工作区中值得多核的运算,无一例外都是**逐元素独立**的循环,用标准库的
`std::thread::scope` 就能直接表达。
作用域线程按引用借用数据,调用方无需 `Arc` 即可共享一个切片或一个只读对象; The workspace forbids every third-party crate, so a pool such as `rayon` is
作用域结束前会 join 全部线程,因此不会有结果在线程仍在写时被读走。 not available. None is needed either: every computation here that is worth
several cores is a loop over independent items, which the standard
library's `std::thread::scope` expresses directly.
## 接口 Scoped threads borrow their data, so a caller shares a slice or a read-only
value without an `Arc`, and the scope joins every thread before it returns,
so no result is read while a thread is still writing it.
### apsis-math:并行原语 ## The interfaces
`apsis_math::parallel` 提供两个入口,是所有并行接口的基础: ### apsis-math: the primitives
| 函数 | 用途 | `apsis_math::parallel` provides the entry points every other interface builds
on:
| Function | Purpose |
| --- | --- | | --- | --- |
| `thread_count()` | 机器的并行度 | | `thread_count()` | the parallelism of the machine |
| `threads_for(len)` | 该长度下值得启用的线程数 | | `threads_for(len)` | the threads worth starting for a length |
| `for_each_index(len, f)` | 对每个下标调用 `f` | | `for_each_index(len, f)` | call `f` for every index |
| `map_index(len, f)` | 每个下标产出一个值,按下标序返回 | | `map_index(len, f)` | produce a value per index, returned in index order |
两个入口都只要求闭包可**同步调用**(`Fn` + `Sync`),因为它们把输入当作共享 Both loop entries require only that the closure can be **called** from
的、把输出写进各自负责的槽位;调用方不需要自己同步。 several threads (`Fn` plus `Sync`), because they treat the input as shared
and the output as a slot each thread owns; the caller synchronises nothing.
`threads_for` 在长度小于每线程一千项时返回 1:在这个量级上,启动线程的成本 `threads_for` returns one below a thousand items per thread, because at that
高于它能省下的工作。 scale starting a thread costs more than the work it would save.
### 各层的批处理接口 ### The batch interfaces of the layers
| crate | 接口 | 适用规模 | | Crate | Interface | Scale it suits |
| --- | --- | --- | | --- | --- | --- |
| `apsis-sgp4` | `propagate_minutes_at_parallel` | 长轨道(≥ 数万样本) | | `apsis-sgp4` | `propagate_minutes_at_parallel` | a long track, tens of thousands of samples or more |
| `apsis-sgp4` | `propagate_at_parallel` | 整壳一次性传播 | | `apsis-sgp4` | `propagate_at_parallel` | a whole shell in one call |
| `apsis-access` | `find_all_parallel` | 数百条轨道 | | `apsis-access` | `find_all_parallel` | hundreds of tracks |
| `apsis-isl` | `visibility_matrix_parallel` | 数百颗以上(见下) | | `apsis-isl` | `visibility_matrix_parallel` | a few hundred satellites or more, see below |
每个接口都有一个同名的顺序版本,两者结果逐元素相同、顺序相同,调用方可自由 Each interface has a sequential counterpart of the same name. The two agree
切换。 element for element and in the same order, so a caller may switch freely.
## 判定标准 ## When a loop is worth threading
并非所有批处理都值得并行化。判定依据是**单次工作成本**与**总量**: Not every batch is worth threading. The decision rests on the **cost of one
item** and the **size of the batch**:
- **值得并行**:单次工作昂贵(≥ 微秒),且逐元素独立,规模可达数百次以上。 - **Worth threading**: the work per item is expensive, at a microsecond or
典型是 SGP4 传播(约 230 ns/样本,百万样本量级)与可见性搜索(约 7 μs/轨)。 more, the items are independent, and the batch reaches hundreds. The
- **不值得并行**: typical cases are SGP4 propagation at about 230 ns a sample over millions
- `Constellation::instantiate`:纯算术,几百颗星只需微秒,线程开销完全淹没。 of samples, and the access search at about 7 us a track.
- 调度器:贪心放置本质串行,每次放置改变后续任务的可行区间,不是独立 - **Not worth threading**:
循环,因此没有并行接口;但其中的搜索已改为单次扫描(见下)。 - `Constellation::instantiate`: pure arithmetic, microseconds for a few
- 事件引擎:单队列时序,无法分块。 hundred satellites, which the thread overhead entirely drowns.
- 大地与坐标变换:单点调用本身已是 O(1),没有批量。 - The scheduler: a greedy placement is serial in substance, since each
- `mutual_visibility` 单次:约 4 ns,**单次**不值得,但成矩阵(见下)。 placement changes the feasible interval of the tasks after it, so the
loop is not independent and there is no parallel interface. Its search
is nonetheless much faster than it was, as the last section describes.
- The event engine: one queue in time order, which cannot be split.
- The geodetic and coordinate transforms: a single call is already
constant time, so there is no batch.
- One `mutual_visibility` call: about 4 ns, which is not worth threading
on its own but is as a matrix, as below.
## 实测(16 核,release) ## Measurements (sixteen cores, release)
### SGP4 单条轨道(`propagate_minutes_at_parallel`) ### One satellite track (`propagate_minutes_at_parallel`)
| 轨道长度 | 顺序 | 并行 | 加速 | | Track length | Sequential | Parallel | Speed-up |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 1 天(1,440 样本) | 0.3 ms | 0.3 ms | 1.28× | | 1 day (1,440 samples) | 0.3 ms | 0.3 ms | 1.28x |
| 7 天(10,080) | 2.3 ms | 0.9 ms | 2.46× | | 7 days (10,080) | 2.3 ms | 0.9 ms | 2.46x |
| 30 天(43,200) | 10.1 ms | 1.9 ms | 5.45× | | 30 days (43,200) | 10.1 ms | 1.9 ms | 5.45x |
| 365 天(525,600) | 144.1 ms | 26.0 ms | 5.54× | | 365 days (525,600) | 144.1 ms | 26.0 ms | 5.54x |
**结论**:轨道越长加速越高,30 天以上稳定在 5.5×。一天以内收益很小—— **Conclusion**: the longer the track the larger the gain, settling near 5.5x
1,440 个样本分给多线程后每线程不足一千项,低于 `threads_for` 的阈值。 past a month. A day or less gains little, because splitting 1,440 samples
across the threads leaves each with fewer than the thousand items
`threads_for` asks for.
**实践要点**:有大量短轨道时,应使用 `propagate_at_parallel`(整壳一次调用), **Practical point**: with many short tracks, call `propagate_at_parallel` for
而不是对每颗星各调一次并行版——后者会让每颗星都付一次线程启动成本。 the whole shell rather than the parallel form once per satellite, because the
latter pays the thread start-up once per satellite.
### 可见性矩阵(`visibility_matrix_parallel`) ### The visibility matrix (`visibility_matrix_parallel`)
| 星数 | 对数 | 顺序 | 并行 | 加速 | | Satellites | Pairs | Sequential | Parallel | Speed-up |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| 200 | 4 万 | 0.16 ms | 0.31 ms | 0.53× | | 200 | 40 thousand | 0.16 ms | 0.31 ms | 0.53x |
| 600 | 36 万 | 1.50 ms | 1.37 ms | 1.09× | | 600 | 360 thousand | 1.50 ms | 1.37 ms | 1.09x |
| 1,000 | 100 万 | 4.09 ms | 2.50 ms | 1.64× | | 1,000 | 1 million | 4.09 ms | 2.50 ms | 1.64x |
**结论**:单次可见性判定只有几纳秒,**小规模下并行反而更慢**(线程开销占比高), **Conclusion**: one visibility test costs a few nanoseconds, so **below a
数百颗星以上才开始获益,且远未饱和。这个接口是为大规模壳准备的;小壳应直接用 few hundred satellites the parallel form is slower**, because the thread
顺序版。接口文档也如实说明了这一点。 overhead dominates. It only starts to gain past that and is nowhere near
saturated. The interface exists for a large shell; a small one should use
the sequential form, and the interface documentation says so.
### 跨层组合 ### Across layers
`bench_parallel` 的测量显示,整壳在多核上传播与构建拓扑的并行效果最好,因为 The measurements in `bench_parallel` show that propagating a whole shell and
它把整个工作量一次性摊给所有线程。相关数据见该示例的输出。 building a topology benefit most, because they hand the entire amount of work
to the threads at once. Run the example for the figures.
## 确定性 ## Determinism
并行不改变结果:所有批量接口都逐元素比对顺序版的结果(SGP4 的状态、可见性 Threading does not change the result. Every batch interface compares its
的布尔矩阵、access 的过站摘要),顺序也一致。传播本身是确定性的,只依赖时刻, output element by element against the sequential form, including the order:
与如何分块无关;可见性与过站搜索是逐元素独立的。因此同一输入在任意线程数下 the states from SGP4, the boolean matrix from visibility, the pass summaries
产生相同输出,测试也按此断言。 from access. Propagation is deterministic and depends only on the instant, not
on how it was split, and visibility and the pass search are independent per
item. The same input therefore gives the same output at any thread count, and
the tests assert exactly that.
## 调度器搜索的优化 ## The scheduler search
调度器没有并行接口,但它的最坏情况曾达到每任务数百微秒。优化分两步,两者都 The scheduler has no parallel interface, but its worst case once cost
保持结果逐位不变。 hundreds of microseconds a task. The fix came in two steps, neither of which
changes a single bit of the result.
**第一步:一次扫描代替逐候选测试。** 旧的做法列出所有"可行性可能变化"的时刻, **First, one sweep in place of a test per candidate.** The search used to
逐个测试,每次都重新计算每个资源的负载曲线。负载曲线与候选时刻无关,因此改为 list every instant at which feasibility could change and test them one by
扫一次:把"资源没有余量的区间"从可用窗口中减去,多资源求交,取第一个存活区间 one, recomputing the load profile of every resource at each step. The profile
的起点。结果仍由 `feasible_at` 确认,枚举法保留为浮点边界不一致时的回退。 does not depend on the candidate, so it is now swept once: the intervals
where a resource has no room are cut from its availability windows, the
per-resource results are intersected, and the first surviving start is read
off the front. The answer is still confirmed by `feasible_at`, and the
enumeration remains as the fallback for the rare case where rounding at a
capacity boundary makes the two disagree.
**第二步:确认调用复用索引。** 去掉确认调用后耗时降到八分之一,这定位了真正 **Second, the confirmation reuses the index.** Removing the confirmation
的开销:确认用的公开可行性测试在收集边界时扫描一次任务表,又在每个边界上对每 entirely cut the crowded case by eight, which located the real cost: the
个覆盖该边界的放置再扫描一次任务表。把第一步建好的索引传进去,两处扫描都变成 public feasibility test that does the confirming scans the task list once to
按标识符直接读取。 collect the boundaries and then, for every boundary, once more to sum the
demand of each placement covering it. Passing the index the sweep already
built turns both scans into a read by identifier.
任务标识符是小而稠密的整数,因此索引用向量而非哈希表:数组下标比哈希一个整数 A task identifier is a small dense integer, so the index is a vector rather
更便宜,这在本仓库已测到两次(拓扑的邻接表用索引、这里的任务表用向量)。 than a map: an array index is cheaper than hashing a small integer, which
this repository has now measured twice, once for the adjacency index in
`apsis-topology` and once here.
**结果**:全部任务争用单一资源、容量为瓶颈时,3200 个任务从 6.7 秒降到 0.48 **Result**: with every task contending for one resource and the capacity the
秒(14 倍),6400 个任务 2.2 秒。计划结果不变——测试在两种资源、窗口不相交的 binding constraint, planning three thousand two hundred tasks falls from 6.7
场景下逐个比对了每个放置的起止时刻。 seconds to 0.48, a factor of fourteen, and six thousand four hundred takes
2.2 seconds. The plan is unchanged: a test compares the start and end of
every placement before and after, across two resources with disjoint
availability windows.

123
docs/design/parallel.zh.md Normal file
View File

@ -0,0 +1,123 @@
# 并行化
[English](./parallel.md) | 简体中文
本文件记录并行化的判定标准、提供的接口,以及实测结果。跨层的单位与坐标约定见
[单位与坐标约定](units.md),分层边界见 [分层边界](layers.md)。
## 为什么不引入线程池
工作区禁止任何第三方 crate,因此没有 `rayon` 这样的线程池可用。也不需要:
工作区中值得多核的运算,无一例外都是**逐元素独立**的循环,用标准库的
`std::thread::scope` 就能直接表达。
作用域线程按引用借用数据,调用方无需 `Arc` 即可共享一个切片或一个只读对象;
作用域结束前会 join 全部线程,因此不会有结果在线程仍在写时被读走。
## 接口
### apsis-math:并行原语
`apsis_math::parallel` 提供两个入口,是所有并行接口的基础:
| 函数 | 用途 |
| --- | --- |
| `thread_count()` | 机器的并行度 |
| `threads_for(len)` | 该长度下值得启用的线程数 |
| `for_each_index(len, f)` | 对每个下标调用 `f` |
| `map_index(len, f)` | 每个下标产出一个值,按下标序返回 |
两个入口都只要求闭包可**同步调用**(`Fn` + `Sync`),因为它们把输入当作共享
的、把输出写进各自负责的槽位;调用方不需要自己同步。
`threads_for` 在长度小于每线程一千项时返回 1:在这个量级上,启动线程的成本
高于它能省下的工作。
### 各层的批处理接口
| crate | 接口 | 适用规模 |
| --- | --- | --- |
| `apsis-sgp4` | `propagate_minutes_at_parallel` | 长轨道(≥ 数万样本) |
| `apsis-sgp4` | `propagate_at_parallel` | 整壳一次性传播 |
| `apsis-access` | `find_all_parallel` | 数百条轨道 |
| `apsis-isl` | `visibility_matrix_parallel` | 数百颗以上(见下) |
每个接口都有一个同名的顺序版本,两者结果逐元素相同、顺序相同,调用方可自由
切换。
## 判定标准
并非所有批处理都值得并行化。判定依据是**单次工作成本**与**总量**:
- **值得并行**:单次工作昂贵(≥ 微秒),且逐元素独立,规模可达数百次以上。
典型是 SGP4 传播(约 230 ns/样本,百万样本量级)与可见性搜索(约 7 μs/轨)。
- **不值得并行**:
- `Constellation::instantiate`:纯算术,几百颗星只需微秒,线程开销完全淹没。
- 调度器:贪心放置本质串行,每次放置改变后续任务的可行区间,不是独立
循环,因此没有并行接口;但其中的搜索已改为单次扫描(见下)。
- 事件引擎:单队列时序,无法分块。
- 大地与坐标变换:单点调用本身已是 O(1),没有批量。
- `mutual_visibility` 单次:约 4 ns,**单次**不值得,但成矩阵(见下)。
## 实测(16 核,release)
### SGP4 单条轨道(`propagate_minutes_at_parallel`)
| 轨道长度 | 顺序 | 并行 | 加速 |
| --- | --- | --- | --- |
| 1 天(1,440 样本) | 0.3 ms | 0.3 ms | 1.28× |
| 7 天(10,080) | 2.3 ms | 0.9 ms | 2.46× |
| 30 天(43,200) | 10.1 ms | 1.9 ms | 5.45× |
| 365 天(525,600) | 144.1 ms | 26.0 ms | 5.54× |
**结论**:轨道越长加速越高,30 天以上稳定在 5.5×。一天以内收益很小——
1,440 个样本分给多线程后每线程不足一千项,低于 `threads_for` 的阈值。
**实践要点**:有大量短轨道时,应使用 `propagate_at_parallel`(整壳一次调用),
而不是对每颗星各调一次并行版——后者会让每颗星都付一次线程启动成本。
### 可见性矩阵(`visibility_matrix_parallel`)
| 星数 | 对数 | 顺序 | 并行 | 加速 |
| --- | --- | --- | --- | --- |
| 200 | 4 万 | 0.16 ms | 0.31 ms | 0.53× |
| 600 | 36 万 | 1.50 ms | 1.37 ms | 1.09× |
| 1,000 | 100 万 | 4.09 ms | 2.50 ms | 1.64× |
**结论**:单次可见性判定只有几纳秒,**小规模下并行反而更慢**(线程开销占比高),
数百颗星以上才开始获益,且远未饱和。这个接口是为大规模壳准备的;小壳应直接用
顺序版。接口文档也如实说明了这一点。
### 跨层组合
`bench_parallel` 的测量显示,整壳在多核上传播与构建拓扑的并行效果最好,因为
它把整个工作量一次性摊给所有线程。相关数据见该示例的输出。
## 确定性
并行不改变结果:所有批量接口都逐元素比对顺序版的结果(SGP4 的状态、可见性
的布尔矩阵、access 的过站摘要),顺序也一致。传播本身是确定性的,只依赖时刻,
与如何分块无关;可见性与过站搜索是逐元素独立的。因此同一输入在任意线程数下
产生相同输出,测试也按此断言。
## 调度器搜索的优化
调度器没有并行接口,但它的最坏情况曾达到每任务数百微秒。优化分两步,两者都
保持结果逐位不变。
**第一步:一次扫描代替逐候选测试。** 旧的做法列出所有"可行性可能变化"的时刻,
逐个测试,每次都重新计算每个资源的负载曲线。负载曲线与候选时刻无关,因此改为
扫一次:把"资源没有余量的区间"从可用窗口中减去,多资源求交,取第一个存活区间
的起点。结果仍由 `feasible_at` 确认,枚举法保留为浮点边界不一致时的回退。
**第二步:确认调用复用索引。** 去掉确认调用后耗时降到八分之一,这定位了真正
的开销:确认用的公开可行性测试在收集边界时扫描一次任务表,又在每个边界上对每
个覆盖该边界的放置再扫描一次任务表。把第一步建好的索引传进去,两处扫描都变成
按标识符直接读取。
任务标识符是小而稠密的整数,因此索引用向量而非哈希表:数组下标比哈希一个整数
更便宜,这在本仓库已测到两次(拓扑的邻接表用索引、这里的任务表用向量)。
**结果**:全部任务争用单一资源、容量为瓶颈时,3200 个任务从 6.7 秒降到 0.48
秒(14 倍),6400 个任务 2.2 秒。计划结果不变——测试在两种资源、窗口不相交的
场景下逐个比对了每个放置的起止时刻。

View File

@ -1,18 +1,21 @@
# 仿真层设计 # The simulation layer
本文件记录仿真层(`sim`)的跨 crate 决策。各 crate 的内部设计见其模块文档。 English | [简体中文](./sim.zh.md)
## 组成与依赖 This file records the cross-crate decisions of the simulation layer. The
design inside each crate is in its module documentation.
## Composition and dependencies
```text ```text
apsis-event 事件类型与事件队列 apsis-event the event type and the event queue
apsis-sim-engine 离散事件引擎 apsis-sim-engine the discrete event engine
apsis-scenario 场景定义与文本加载 apsis-scenario scenario definition and text loading
apsis-output 仿真输出记录 apsis-output simulation output recording
apsis-sim 仿真门面(整合上述四个) apsis-sim the facade, which joins the four above
``` ```
依赖方向: The dependency direction:
```text ```text
apsis-event -> apsis-sim-engine -> apsis-sim -> apsis-rs apsis-event -> apsis-sim-engine -> apsis-sim -> apsis-rs
@ -20,66 +23,87 @@ apsis-scenario -----------------> apsis-sim
apsis-output -------------------> apsis-sim apsis-output -------------------> apsis-sim
``` ```
`apsis-sim` 同时依赖 ops 层(`apsis-scheduler`、`apsis-schedule`、 `apsis-sim` also depends on the ops layer (`apsis-scheduler`,
`apsis-task`、`apsis-resource`),因为它要规划场景并读取计划。其余 crate `apsis-schedule`, `apsis-task`, `apsis-resource`), because it plans a
只依赖 foundation。 scenario and reads the plan. The remaining crates depend on foundation alone.
## 事件顺序 ## The order of events
同一时刻的多个事件必须按可复现的顺序投递,否则同一输入会产生不同输出, Several events at the same instant must be delivered in a reproducible
无法与参考实现对拍。约定: order, or the same input would give a different output and there would be
nothing to cross-check against a reference implementation. The convention:
- 每个事件在入队时获得一个递增的序号。 - Every event takes an increasing sequence number when it is scheduled.
- 事件先按时间排序,时间相同则按序号排序,即入队顺序。 - Events are ordered by time, and events at the same time by that number,
- 处理函数在处理某事件时新调度的事件,排在该时刻已有事件之后。 which is the order they were scheduled in.
- An event that a handler schedules while handling another goes behind the
events already queued for that instant.
该顺序由 `apsis-event` 保证,`apsis-sim-engine` 与 `apsis-sim` 直接沿用。 `apsis-event` provides this order and the engine and the facade carry it
序列号由队列分配,调用方无法制造两个同号事件。 through. The queue assigns the numbers, so a caller cannot create two events
with the same one.
## 时间原点 ## The origin of time
事件时间是从仿真原点起算的秒数,与日历无关;需要日历的场景由上层换算。 An event time is a number of seconds from the origin of the simulation and
has nothing to do with a calendar; a scenario that needs one converts it
above this layer.
场景的 `epoch` 是时钟起点。时间线中的事件时间相对场景原点,`apsis-sim` The `epoch` of a scenario is where the clock starts. The times in a timeline
在投递前加上 `epoch`。若不偏移,引擎会在投递首个事件时把时钟回拨。 are relative to that origin and `apsis-sim` adds the `epoch` before
delivering. Without the offset the engine would move the clock backwards when
it delivered the first event.
时间限制按同一绝对时间轴比较:时间晚于上限的事件留在队列中而不执行,因此 A time limit is compared on the same absolute axis: an event later than the
调用方可以查看运行结束时还剩什么。时钟停在最后一个已执行事件上,而不是停在 limit stays in the queue instead of running, so a caller can see what
上限上。 remained when the run ended. The clock stops at the last event it executed
rather than at the limit.
## 事件限制是累计的 ## The event limit is cumulative
`apsis-sim-engine` 的事件上限是引擎整个生命周期的总数,而非单次 `run` The event limit in `apsis-sim-engine` bounds the total for the life of the
的数量。达到上限后再次 `run` 不执行任何事件。这样"上限"始终表示一个不会 engine, not the count of a single `run`. Once the limit is reached a further
被绕过的界,调用方要运行剩余事件需显式提高上限。 `run` executes nothing. The limit is therefore a bound that cannot be
circumvented, and a caller who wants the remaining events to run has to
raise it explicitly.
## 时间线语义 ## What a timeline means
`apsis-sim` 把计划展开为时间线上的起止事件:每个已放置任务贡献一个开始 `apsis-sim` expands a plan into start and end events on a timeline: every
事件和一个结束事件,二者是独立事件,因此引擎能按时间顺序把它们与其他事件 placed task contributes one start and one end, as separate events, so the
正确排序,重叠任务的先后也由时钟决定。 engine can order them in time with everything else and the order of
overlapping tasks follows from the clock.
时间线是计划本身,不是物理模型。开始/结束事件触发时调用什么物理计算由处理 The timeline is the plan, not a physical model. What physical computation a
函数决定,门面只负责排序与记录。 handler runs when a start or end event fires is up to the handler; the
facade only orders and records.
## 场景文本格式 ## The scenario text format
`apsis-scenario` 自带一套零依赖的行式文本格式,用于存储与重新加载。 `apsis-scenario` carries its own line oriented text format with no
解析分三遍(资源、任务、优先级约束),因此 `require` 可以引用文件中稍后 dependency, for storing a scenario and loading it back. Parsing runs in three
定义的资源,`precedence` 可以引用任意顺序定义的任务。每处失败都带行号。 passes, over the resources, the tasks and the precedence constraints, so a
`require` may name a resource defined later in the file and a `precedence`
may name tasks in any order. Every failure carries the line it came from.
`Scenario::to_text` 的输出必须能被 `Scenario::parse` 读回为相等场景, The output of `Scenario::to_text` must parse back with `Scenario::parse` to
即文本形式是往返稳定的;这一点由测试保证。 an equal scenario, so the text form is stable under a round trip, which a
test asserts.
任务的"无窗口"(两端为无穷)无法写入文本,故写为一个包含该任务的有限区间, A task with no window, whose two ends are infinite, cannot be written as
读回后任务仍可用。 text, so it is written as a finite interval that contains the task and is
still usable once read back.
## 确定性 ## Determinism
仿真结果必须可复现,这是对拍的前提。确定性来自三点: A simulation result must be reproducible, because that is what makes a
cross-check possible. Three things give it:
- 事件队列表述的同刻顺序规则; - the same-instant ordering rule of the event queue;
- 规划器对同权任务的稳定排序(见 ops 层); - the stable ordering the planner applies to tasks of equal weight, described
- 记录器保留插入顺序,`Trace::in_time_order` 为稳定排序。 in the ops layer;
- the recorder, which keeps the order of insertion, where
`Trace::in_time_order` is a stable sort.
`integrate_sim` 示例断言同一次准备出的仿真重复运行得到相同轨迹。 The `integrate_sim` example asserts that running one prepared simulation
twice produces the same trace.

87
docs/design/sim.zh.md Normal file
View File

@ -0,0 +1,87 @@
# 仿真层设计
[English](./sim.md) | 简体中文
本文件记录仿真层(`sim`)的跨 crate 决策。各 crate 的内部设计见其模块文档。
## 组成与依赖
```text
apsis-event 事件类型与事件队列
apsis-sim-engine 离散事件引擎
apsis-scenario 场景定义与文本加载
apsis-output 仿真输出记录
apsis-sim 仿真门面(整合上述四个)
```
依赖方向:
```text
apsis-event -> apsis-sim-engine -> apsis-sim -> apsis-rs
apsis-scenario -----------------> apsis-sim
apsis-output -------------------> apsis-sim
```
`apsis-sim` 同时依赖 ops 层(`apsis-scheduler`、`apsis-schedule`、
`apsis-task`、`apsis-resource`),因为它要规划场景并读取计划。其余 crate
只依赖 foundation。
## 事件顺序
同一时刻的多个事件必须按可复现的顺序投递,否则同一输入会产生不同输出,
无法与参考实现对拍。约定:
- 每个事件在入队时获得一个递增的序号。
- 事件先按时间排序,时间相同则按序号排序,即入队顺序。
- 处理函数在处理某事件时新调度的事件,排在该时刻已有事件之后。
该顺序由 `apsis-event` 保证,`apsis-sim-engine` 与 `apsis-sim` 直接沿用。
序列号由队列分配,调用方无法制造两个同号事件。
## 时间原点
事件时间是从仿真原点起算的秒数,与日历无关;需要日历的场景由上层换算。
场景的 `epoch` 是时钟起点。时间线中的事件时间相对场景原点,`apsis-sim`
在投递前加上 `epoch`。若不偏移,引擎会在投递首个事件时把时钟回拨。
时间限制按同一绝对时间轴比较:时间晚于上限的事件留在队列中而不执行,因此
调用方可以查看运行结束时还剩什么。时钟停在最后一个已执行事件上,而不是停在
上限上。
## 事件限制是累计的
`apsis-sim-engine` 的事件上限是引擎整个生命周期的总数,而非单次 `run`
的数量。达到上限后再次 `run` 不执行任何事件。这样"上限"始终表示一个不会
被绕过的界,调用方要运行剩余事件需显式提高上限。
## 时间线语义
`apsis-sim` 把计划展开为时间线上的起止事件:每个已放置任务贡献一个开始
事件和一个结束事件,二者是独立事件,因此引擎能按时间顺序把它们与其他事件
正确排序,重叠任务的先后也由时钟决定。
时间线是计划本身,不是物理模型。开始/结束事件触发时调用什么物理计算由处理
函数决定,门面只负责排序与记录。
## 场景文本格式
`apsis-scenario` 自带一套零依赖的行式文本格式,用于存储与重新加载。
解析分三遍(资源、任务、优先级约束),因此 `require` 可以引用文件中稍后
定义的资源,`precedence` 可以引用任意顺序定义的任务。每处失败都带行号。
`Scenario::to_text` 的输出必须能被 `Scenario::parse` 读回为相等场景,
即文本形式是往返稳定的;这一点由测试保证。
任务的"无窗口"(两端为无穷)无法写入文本,故写为一个包含该任务的有限区间,
读回后任务仍可用。
## 确定性
仿真结果必须可复现,这是对拍的前提。确定性来自三点:
- 事件队列表述的同刻顺序规则;
- 规划器对同权任务的稳定排序(见 ops 层);
- 记录器保留插入顺序,`Trace::in_time_order` 为稳定排序。
`integrate_sim` 示例断言同一次准备出的仿真重复运行得到相同轨迹。

View File

@ -1,97 +1,119 @@
# 单位与坐标约定 # Units and coordinates
本文件记录跨层的单位与坐标约定。单个 crate 的内部约定见其模块文档;这里只 English | [简体中文](./units.zh.md)
记录**跨层边界**必须一致的部分,因为边界处的单位不一致是本仓库最容易引入
也最难发现的错误。
## 长度的两种单位 This file records the unit and coordinate conventions that cross layers. The
conventions inside a single crate are in its module documentation; what
belongs here is the part that must agree **across a layer boundary**, because
a unit that disagrees at a boundary is the easiest mistake to introduce in
this repository and the hardest to find.
仓库同时使用千米与米,边界在 orbit 层与 physics 层之间: ## Two units of length
| 层 / crate | 长度单位 | The repository uses both kilometres and metres, with the boundary between the
orbit layer and the physics layer:
| Layer / crate | Unit of length |
| --- | --- | | --- | --- |
| `apsis-kepler`、`apsis-sgp4`、`apsis-tle` | 千米 | | `apsis-kepler`, `apsis-sgp4`, `apsis-tle` | kilometres |
| `apsis-ephemeris` | 千米 | | `apsis-ephemeris` | kilometres |
| `apsis-maneuver` | 千米 | | `apsis-maneuver` | kilometres |
| `apsis-constellation`、`apsis-coverage`、`apsis-access`、`apsis-isl` | 千米 | | `apsis-constellation`, `apsis-coverage`, `apsis-access`, `apsis-isl` | kilometres |
| `apsis-topology`、`apsis-routing`、`apsis-mesh` | 长度仅在 `apsis-isl` 计算,边的 `cost` 是调用方定义的无量纲值 | | `apsis-topology`, `apsis-routing`, `apsis-mesh` | length is computed only in `apsis-isl`; an edge's `cost` is a dimensionless value the caller defines |
| `apsis-handover` | 不涉及长度;`quality` 为调用方定义,时间用秒 | | `apsis-handover` | no length; `quality` is defined by the caller and time is in seconds |
| `apsis-coord`(`Eci`、`Ecef`) | 米 | | `apsis-coord` (`Eci`, `Ecef`) | metres |
| `apsis-geodesy`(`Geodetic` 高度、椭球半轴、笛卡尔位置) | 米 | | `apsis-geodesy` (geodetic height, ellipsoid semi-axes, Cartesian position) | metres |
| `apsis-frames` | 无量纲(角度与旋转) | | `apsis-frames` | dimensionless (angles and rotations) |
| `apsis-rf`、`apsis-antenna`、`apsis-atmosphere`、`apsis-noise`、`apsis-doppler`、`apsis-interference`、`apsis-link` | 米、赫兹 | | `apsis-rf`, `apsis-antenna`, `apsis-atmosphere`, `apsis-noise`, `apsis-doppler`, `apsis-interference`, `apsis-link` | metres, hertz |
选择千米还是米并非任意:卫星轨道用千米表达更自然(`apsis-sgp4` 输出的量级 The choice is not arbitrary. Satellite orbits read naturally in kilometres,
是几千),射频链路用米表达更自然(波长与距离用米,与 ITU-R 建议书一致)。 which is the magnitude `apsis-sgp4` produces, and a radio link reads
代价是边界处必须显式换算。 naturally in metres, which is what the ITU-R recommendations use. The cost is
that the boundary needs an explicit conversion.
### 边界处的换算 ### Converting at the boundary
从 orbit 层进入 physics 层时乘以 `1000.0`,返回时除以 `1000.0`: Multiply by `1000.0` on the way from the orbit layer into the physics layer,
and divide on the way back:
```rust ```rust
// orbit 层的千米位置转 physics 层的米位置。 // A position in kilometres from the orbit layer becomes metres in physics.
let position_km = satellite.propagate_minutes(0.0)?.position; // 千米 let position_km = satellite.propagate_minutes(0.0)?.position; // kilometres
let ecef = Ecef::new(position_km * 1_000.0); // 米 let ecef = Ecef::new(position_km * 1_000.0); // metres
``` ```
漏掉这一步会把卫星放到地球内部,而且数值仍然"看起来合理",不会立刻报错。 Omitting this step puts the satellite inside the Earth, and the numbers still
`integrate_network` 示例对这一边界做了断言:换算后的半径必须与传播器报告 look plausible rather than failing at once. The `integrate_network` example
的半径一致,这是唯一能自动发现漏乘或漏除的检查。 asserts on this boundary: the radius after the conversion must match the
radius the propagator reports, which is the only check that finds a missing
multiply or divide automatically.
## 角度 ## Angles
**所有计算用角度一律为弧度**。纬度、经度、倾角、升交点赤经、近地点幅角、 **Every angle used in a calculation is in radians.** Latitude, longitude,
真近点角、仰角、方位角、天线方向图角都是弧度。 inclination, right ascension of the ascending node, argument of perigee, true
anomaly, elevation, azimuth and antenna pattern angle are all radians.
唯一的例外是两线元素集:`apsis-tle::TwoLineElement` 的原始字段按 TLE 格式 The only exception is the two-line element set, whose raw fields are in
用度与每日圈数,因为那是文件的编码。`apsis-sgp4` 在初始化时换算为弧度与 degrees and revolutions per day because that is how the file encodes them.
弧度每分钟。 `apsis-sgp4` converts them to radians and radians per minute on
initialisation.
## 时间 ## Time
时间统一用秒表示,原点由调用方选择: Time is in seconds throughout, with the origin chosen by the caller:
- ops 与 sim 层:从场景原点(scenario epoch)起算的秒数。 - The ops and sim layers count seconds from the scenario epoch.
- orbit 层:`apsis-sgp4` 用"距元素历元的分钟数",`apsis-ephemeris` 与 - The orbit layer uses minutes from the element set epoch in `apsis-sgp4`;
其余 crate 用秒。 `apsis-ephemeris` and the rest use seconds.
- 日历时间:`apsis-time` 的 `Epoch` 存 TAI 儒略日;UTC 换算经 `apsis-leap` - Calendar time: `apsis-time::Epoch` holds a TAI Julian date, and a
的闰秒表。 conversion to UTC goes through the leap second table in `apsis-leap`.
`Orbit::sample_ephemeris` 是两种时间表示的边界:入参是距元素历元的分钟, `Orbit::sample_ephemeris` is the boundary between the two representations: it
返回的星历内部用秒,换算在方法内完成。 takes minutes from the element epoch and the ephemeris it returns is in
seconds, with the conversion done inside the method.
## 坐标框架 ## Coordinate frames
| 框架 | 类型 | 说明 | | Frame | Type | Notes |
| --- | --- | --- | | --- | --- | --- |
| TEME | `apsis_sgp4::State` | SGP4 定义的真赤道平春分点框架 | | TEME | `apsis_sgp4::State` | the true equator, mean equinox frame the SGP4 model defines |
| ECI | `apsis_coord::Eci` | 惯性笛卡尔坐标 | | ECI | `apsis_coord::Eci` | inertial Cartesian coordinates |
| ECEF | `apsis_coord::Ecef` | 地固笛卡尔坐标 | | ECEF | `apsis_coord::Ecef` | Earth-fixed Cartesian coordinates |
| 大地坐标 | `apsis_geodesy::Geodetic` | 纬度、经度、大地高 | | Geodetic | `apsis_geodesy::Geodetic` | latitude, longitude, height above the ellipsoid |
| 地面点 | `apsis_coverage::GroundPoint` | 大地坐标加仰角遮蔽 | | Ground point | `apsis_coverage::GroundPoint` | a geodetic position plus an elevation mask |
TEME 到 ECEF 需要地球自转角(`apsis-frames::EarthRotationAngle`)加上岁差 Going from TEME to ECEF needs the Earth rotation angle
章动;`apsis-coord` 提供变换,`apsis-frames` 提供角。TEME 与 ECI 的差别是 (`apsis-frames::EarthRotationAngle`) together with precession and nutation.
分点方程,量级低于千分之一度;`integrate_network` 示例中直接把它当作惯性 `apsis-coord` provides the transformation and `apsis-frames` provides the
系处理,并注明这是该示例的近似。 angle. TEME differs from ECI by the equation of the equinoxes, which is below
a thousandth of a degree; the `integrate_network` example treats it as the
inertial frame and says so, because for that example it is an approximation.
## 其它量 ## Other quantities
- 引力参数:`apsis-kepler::GravityParameter`,单位 km³/s²。WGS 72 对应 - The gravitational parameter is `apsis-kepler::GravityParameter`, in
SGP4,WGS 84 对应其余用途;选错会使密切半长轴偏约百万分之一。门面 cubic kilometres per second squared. WGS 72 goes with SGP4 and WGS 84 with
`apsis-orbit::Orbit` 把引力参数与传播器绑定,避免这一错误。 everything else; choosing the wrong one shifts the osculating semi-major
- 速度:与长度同层,km/s 或 m/s。 axis by about one part in a million. The `apsis-orbit::Orbit` facade binds
- 功率用瓦、频率用赫兹、温度用开尔文;分贝为功率比,`dBi` 相对各向同性、 the parameter to the propagator so that this cannot go wrong.
`dBd` 相对半波偶极子(相差 2.15 dB)。 - Velocity follows the unit of length in its own layer, so km/s or m/s.
- 相对量一律无量纲:偏心率、填充率、利用率、信噪比的分贝值。 - Power is in watts, frequency in hertz and temperature in kelvin. A decibel
is a power ratio; `dBi` is relative to an isotropic radiator and `dBd` to a
half-wave dipole, which is 2.15 dB more directive.
- Every relative quantity is dimensionless: eccentricity, fill rate,
utilisation and a signal to noise ratio in decibels.
## 检查 ## Checks
单位边界没有类型系统保护,靠测试与文档: Nothing in the type system protects a unit boundary, so the tests and this
document carry it:
- 各层测试覆盖层内换算。 - Each layer's tests cover the conversions inside that layer.
- `integrate_network` 断言千米与米的边界换算保持半径。 - `integrate_network` asserts that the kilometre to metre boundary conversion
- `integrate_ops`、`integrate_sim` 覆盖 ops 与 sim 层的时间轴约定。 preserves the radius.
- `cargo run --release -p apsis-rs --features full --example bench` 覆盖 - `integrate_ops` and `integrate_sim` cover the time axis conventions of the
各层热点路径,任一层的接口变更导致编译失败都会在此暴露。 ops and sim layers.
- `cargo run --release -p apsis-rs --features full --example bench` touches
the hot path of every layer, so an interface change in any of them shows up
there as a build failure.

99
docs/design/units.zh.md Normal file
View File

@ -0,0 +1,99 @@
# 单位与坐标约定
[English](./units.md) | 简体中文
本文件记录跨层的单位与坐标约定。单个 crate 的内部约定见其模块文档;这里只
记录**跨层边界**必须一致的部分,因为边界处的单位不一致是本仓库最容易引入
也最难发现的错误。
## 长度的两种单位
仓库同时使用千米与米,边界在 orbit 层与 physics 层之间:
| 层 / crate | 长度单位 |
| --- | --- |
| `apsis-kepler`、`apsis-sgp4`、`apsis-tle` | 千米 |
| `apsis-ephemeris` | 千米 |
| `apsis-maneuver` | 千米 |
| `apsis-constellation`、`apsis-coverage`、`apsis-access`、`apsis-isl` | 千米 |
| `apsis-topology`、`apsis-routing`、`apsis-mesh` | 长度仅在 `apsis-isl` 计算,边的 `cost` 是调用方定义的无量纲值 |
| `apsis-handover` | 不涉及长度;`quality` 为调用方定义,时间用秒 |
| `apsis-coord`(`Eci`、`Ecef`) | 米 |
| `apsis-geodesy`(`Geodetic` 高度、椭球半轴、笛卡尔位置) | 米 |
| `apsis-frames` | 无量纲(角度与旋转) |
| `apsis-rf`、`apsis-antenna`、`apsis-atmosphere`、`apsis-noise`、`apsis-doppler`、`apsis-interference`、`apsis-link` | 米、赫兹 |
选择千米还是米并非任意:卫星轨道用千米表达更自然(`apsis-sgp4` 输出的量级
是几千),射频链路用米表达更自然(波长与距离用米,与 ITU-R 建议书一致)。
代价是边界处必须显式换算。
### 边界处的换算
从 orbit 层进入 physics 层时乘以 `1000.0`,返回时除以 `1000.0`:
```rust
// orbit 层的千米位置转 physics 层的米位置。
let position_km = satellite.propagate_minutes(0.0)?.position; // 千米
let ecef = Ecef::new(position_km * 1_000.0); // 米
```
漏掉这一步会把卫星放到地球内部,而且数值仍然"看起来合理",不会立刻报错。
`integrate_network` 示例对这一边界做了断言:换算后的半径必须与传播器报告
的半径一致,这是唯一能自动发现漏乘或漏除的检查。
## 角度
**所有计算用角度一律为弧度**。纬度、经度、倾角、升交点赤经、近地点幅角、
真近点角、仰角、方位角、天线方向图角都是弧度。
唯一的例外是两线元素集:`apsis-tle::TwoLineElement` 的原始字段按 TLE 格式
用度与每日圈数,因为那是文件的编码。`apsis-sgp4` 在初始化时换算为弧度与
弧度每分钟。
## 时间
时间统一用秒表示,原点由调用方选择:
- ops 与 sim 层:从场景原点(scenario epoch)起算的秒数。
- orbit 层:`apsis-sgp4` 用"距元素历元的分钟数",`apsis-ephemeris` 与
其余 crate 用秒。
- 日历时间:`apsis-time` 的 `Epoch` 存 TAI 儒略日;UTC 换算经 `apsis-leap`
的闰秒表。
`Orbit::sample_ephemeris` 是两种时间表示的边界:入参是距元素历元的分钟,
返回的星历内部用秒,换算在方法内完成。
## 坐标框架
| 框架 | 类型 | 说明 |
| --- | --- | --- |
| TEME | `apsis_sgp4::State` | SGP4 定义的真赤道平春分点框架 |
| ECI | `apsis_coord::Eci` | 惯性笛卡尔坐标 |
| ECEF | `apsis_coord::Ecef` | 地固笛卡尔坐标 |
| 大地坐标 | `apsis_geodesy::Geodetic` | 纬度、经度、大地高 |
| 地面点 | `apsis_coverage::GroundPoint` | 大地坐标加仰角遮蔽 |
TEME 到 ECEF 需要地球自转角(`apsis-frames::EarthRotationAngle`)加上岁差
章动;`apsis-coord` 提供变换,`apsis-frames` 提供角。TEME 与 ECI 的差别是
分点方程,量级低于千分之一度;`integrate_network` 示例中直接把它当作惯性
系处理,并注明这是该示例的近似。
## 其它量
- 引力参数:`apsis-kepler::GravityParameter`,单位 km³/s²。WGS 72 对应
SGP4,WGS 84 对应其余用途;选错会使密切半长轴偏约百万分之一。门面
`apsis-orbit::Orbit` 把引力参数与传播器绑定,避免这一错误。
- 速度:与长度同层,km/s 或 m/s。
- 功率用瓦、频率用赫兹、温度用开尔文;分贝为功率比,`dBi` 相对各向同性、
`dBd` 相对半波偶极子(相差 2.15 dB)。
- 相对量一律无量纲:偏心率、填充率、利用率、信噪比的分贝值。
## 检查
单位边界没有类型系统保护,靠测试与文档:
- 各层测试覆盖层内换算。
- `integrate_network` 断言千米与米的边界换算保持半径。
- `integrate_ops`、`integrate_sim` 覆盖 ops 与 sim 层的时间轴约定。
- `cargo run --release -p apsis-rs --features full --example bench` 覆盖
各层热点路径,任一层的接口变更导致编译失败都会在此暴露。

View File

@ -1,109 +1,118 @@
# 对拍验证 # Cross-check validation
本目录用于 Rust 实现与公开参考实现的数值对拍。Python 侧仅作验证用途, English | [简体中文](./README.zh.md)
不属于 Rust 依赖,也不参与 Rust 编译。
## Python 环境 This directory holds the numerical cross-checks of the Rust implementation
against public reference implementations. The Python side is for validation
only: it is not a Rust dependency and takes no part in the Rust build.
使用 [pixi](https://pixi.sh/) 管理: ## The Python environment
It is managed with [pixi](https://pixi.sh/):
```bash ```bash
cd validation cd validation
pixi install pixi install
``` ```
已允许的库: The libraries that are allowed:
- `sgp4`:SGP4 传播对拍 - `sgp4`: the SGP4 propagation cross-check
- `astropy`:时间、坐标对拍 - `astropy`: the time and coordinate cross-checks
- `numpy`:数值计算 - `numpy`: numerical computation
- `skyfield`:轨道、可见性对拍(可选) - `skyfield`: the orbit and visibility cross-checks, optional
- `itur`:ITU-R 建议书参考实现(P.676、P.838、P.618),用于大气与雨衰对拍 - `itur`: the ITU-R reference implementations of P.676, P.838 and P.618, for
the atmosphere and rain cross-checks
引入新的 Python 库前需要评估并确认。 Adding a new Python library needs to be assessed and agreed first.
## 目录 ## Layout
```text ```text
validation/ validation/
├── pixi.toml ├── pixi.toml
├── python/ ├── python/
│ ├── orbit/ # SGP4、开普勒对拍 │ ├── orbit/ # SGP4 and two-body cross-checks
│ ├── time/ # 时间尺度、儒略日对拍 │ ├── time/ # time scale and Julian date cross-checks
│ ├── coord/ # 坐标变换对拍 │ ├── coord/ # coordinate transform cross-checks
│ ├── link/ # 链路预算对拍 │ ├── link/ # link budget cross-checks
│ └── constellation/ # 覆盖几何对拍 │ └── constellation/ # coverage geometry cross-checks
└── data/ └── data/
└── expected/ # Rust 侧导出的待比对数据 └── expected/ # data dumped by the Rust side for comparison
``` ```
## 流程 ## The flow
Rust 侧通过 `apsis-rs` 的 example 导出数据,输出为 CSV(手写格式化,不引 serde): The Rust side writes the data through an example of `apsis-rs`, as CSV on
standard output, formatted by hand without pulling in serde:
```bash ```bash
# 大地坐标 -> ECEF # Geodetic -> ECEF
cargo run -p apsis-rs --features physics --example dump_geodesy \ cargo run -p apsis-rs --features physics --example dump_geodesy \
> validation/data/expected/rust_geodetic.csv > validation/data/expected/rust_geodetic.csv
# 时间尺度换算 # Time scale conversions
cargo run -p apsis-rs --features physics --example dump_time \ cargo run -p apsis-rs --features physics --example dump_time \
> validation/data/expected/rust_time.csv > validation/data/expected/rust_time.csv
# SGP4/SDP4 传播 # SGP4 and SDP4 propagation
cargo run -p apsis-rs --features orbit --example dump_orbit \ cargo run -p apsis-rs --features orbit --example dump_orbit \
> validation/data/expected/rust_sgp4.csv > validation/data/expected/rust_sgp4.csv
# 链路预算 # Link budget
cargo run -p apsis-rs --features rf --example dump_link \ cargo run -p apsis-rs --features rf --example dump_link \
> validation/data/expected/rust_link.csv > validation/data/expected/rust_link.csv
# ITU-R P.676-12 气体吸收 # ITU-R P.676-12 gaseous absorption
cargo run -p apsis-rs --features rf --example dump_gas \ cargo run -p apsis-rs --features rf --example dump_gas \
> validation/data/expected/rust_gas.csv > validation/data/expected/rust_gas.csv
# ITU-R P.838-3 雨衰系数 # ITU-R P.838-3 rain attenuation coefficients
cargo run -p apsis-rs --features rf --example dump_rain \ cargo run -p apsis-rs --features rf --example dump_rain \
> validation/data/expected/rust_rain.csv > validation/data/expected/rust_rain.csv
# ITU-R P.618-13 雨衰斜路径 # ITU-R P.618-13 rain on a slant path
cargo run -p apsis-rs --features rf --example dump_rain_path \ cargo run -p apsis-rs --features rf --example dump_rain_path \
> validation/data/expected/rust_rain_path.csv > validation/data/expected/rust_rain_path.csv
# ITU-R S.465 参考天线方向图 # ITU-R S.465 reference antenna pattern
cargo run -p apsis-rs --features rf --example dump_s465 \ cargo run -p apsis-rs --features rf --example dump_s465 \
> validation/data/expected/rust_s465.csv > validation/data/expected/rust_s465.csv
# ITU-R P.839-4 零度等温线高度 # ITU-R P.839-4 zero degree isotherm height
cargo run -p apsis-rs --features rf --example dump_p839 \ cargo run -p apsis-rs --features rf --example dump_p839 \
> validation/data/expected/rust_p839.csv > validation/data/expected/rust_p839.csv
# ITU-R P.676-12 精确斜路径(分层积分 + 折射率射线追踪) # ITU-R P.676-12 exact slant path, layered integration with a refractive
# index ray trace
cargo run -p apsis-rs --features rf --example dump_gas_exact \ cargo run -p apsis-rs --features rf --example dump_gas_exact \
> validation/data/expected/rust_gas_exact.csv > validation/data/expected/rust_gas_exact.csv
# IAU 2000/2006 地球定向(岁差、章动、框架偏移、GCRS→ITRF) # IAU 2000/2006 Earth orientation: precession, nutation, frame offsets,
# GCRS to ITRF
cargo run -p apsis-rs --features physics --example dump_iau2000 \ cargo run -p apsis-rs --features physics --example dump_iau2000 \
> validation/data/expected/rust_iau2000.csv > validation/data/expected/rust_iau2000.csv
# TEME → 地固系(SGP4 输出链路) # TEME to the Earth-fixed frame, the SGP4 output chain
cargo run -p apsis-rs --features physics --example dump_teme \ cargo run -p apsis-rs --features physics --example dump_teme \
> validation/data/expected/rust_teme.csv > validation/data/expected/rust_teme.csv
# 覆盖几何(仰角、星下点角、地心角、斜距、覆盖面积) # Coverage geometry: elevation, nadir angle, central angle, slant range and
# footprint area
cargo run -p apsis-rs --features network --example dump_coverage \ cargo run -p apsis-rs --features network --example dump_coverage \
> validation/data/expected/rust_coverage.csv > validation/data/expected/rust_coverage.csv
# 二体模型(开普勒要素↔状态矢量,往返转换) # The two-body model: elements to a state vector and back
cargo run -p apsis-rs --features orbit --example dump_kepler \ cargo run -p apsis-rs --features orbit --example dump_kepler \
> validation/data/expected/rust_kepler.csv > validation/data/expected/rust_kepler.csv
# 两线元素集解析(字段、历元展开、模十校验和) # Two-line element parsing: fields, epoch expansion, modulo ten checksum
cargo run -p apsis-rs --features orbit --example dump_tle \ cargo run -p apsis-rs --features orbit --example dump_tle \
> validation/data/expected/rust_tle.csv > validation/data/expected/rust_tle.csv
``` ```
Python 侧读取同一批输入,用参考实现重算并输出误差表: The Python side reads the same inputs, recomputes them with a reference
implementation, and reports the differences:
```bash ```bash
pixi run python validation/python/coord/check_geodetic.py pixi run python validation/python/coord/check_geodetic.py
@ -123,77 +132,96 @@ pixi run python validation/python/orbit/check_kepler.py
pixi run python validation/python/orbit/check_tle.py pixi run python validation/python/orbit/check_tle.py
``` ```
两个脚本都以退出码 0 表示通过、1 表示超差、2 表示缺少输入文件。 Every script exits with zero to pass, one to exceed the tolerance, and two
when the input file is missing.
## 容差 ## Tolerances
| 量 | 容差 | | Quantity | Tolerance |
| --- | --- | | --- | --- |
| 位置、距离 | < 1e-3 km(SGP4 传播)或 1e-9 km(几何量) | | Position and distance | < 1e-3 km for SGP4 propagation, 1e-9 km for a geometric quantity |
| 速度 | < 1e-6 km/s | | Velocity | < 1e-6 km/s |
| 时间 | < 1e-6 s | | Time | < 1e-6 s |
| 角度(覆盖几何) | 1e-12 rad | | Coverage angle | 1e-12 rad |
| 面积、覆盖占比 | 相对 1e-12 | | Area and coverage fraction | 1e-12 relative |
| 链路预算、衰减 | 1e-6 至 1e-9 dB,视模型而定 | | Link budget and attenuation | 1e-6 to 1e-9 dB, depending on the model |
| 地球定向矩阵 | 1e-9 | | Earth orientation matrix | 1e-9 |
各脚本把容差写成其源文件顶部的具名常量,值按所比较的物理量设定,见 Each script names its tolerance as a constant at the top of its source, set
`docs/design/errors.md` 的"对拍容差"一节。 by the physical quantity it compares; see the "Tolerances of the
cross-checks" section of `docs/design/errors.md`.
超差时修正 Rust 侧算法,不放宽容差,除非经确认。 When a check exceeds its tolerance the fix is in the Rust algorithm. The
tolerance is not widened unless that has been agreed.
脚本必须可独立离线运行,不访问网络。`check_time.py` 会关闭 astropy 的 Every script must run offline on its own and reach no network.
Earth orientation 数据自动下载;本 crate 涉及的换算不需要该数据。 `check_time.py` disables the astropy Earth orientation download, which none
of the conversions in this crate needs.
## 已实现的检查 ## The checks that exist
| 脚本 | 对比内容 | 参考实现 | 当前最差误差 | | Script | What it compares | Reference | Worst difference |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `coord/check_geodetic.py` | `Geodetic` → ECEF,280 个点,覆盖全球纬度、经度与高度 | `astropy.coordinates.EarthLocation` | 1.1e-8 m | | `coord/check_geodetic.py` | `Geodetic` to ECEF, 280 points across latitude, longitude and height | `astropy.coordinates.EarthLocation` | 1.1e-8 m |
| `time/check_time.py` | UTC → TAI,117 个时刻,覆盖全部闰秒边界 | `astropy.time.Time` | 5.3e-7 s | | `time/check_time.py` | UTC to TAI, 117 instants covering every leap second boundary | `astropy.time.Time` | 5.3e-7 s |
| `orbit/check_sgp4.py` | SGP4/SDP4 传播,4 组 TLE(近地、高偏心、深空、GTO)× 9 个时刻 | `sgp4` 参考库 | 位置 1.2e-8 km,速度 5.3e-12 km/s | | `orbit/check_sgp4.py` | SGP4 and SDP4 propagation, 4 element sets (near Earth, highly eccentric, deep space, GTO) at 9 instants | the `sgp4` reference library | position 1.2e-8 km, velocity 5.3e-12 km/s |
| `link/check_link.py` | 链路预算,4 个场景(Ku 下行、S 波段、Ka 宽带、L 波段) | 链路方程闭式解(Python 手写) | 5.0e-13 dB | | `link/check_link.py` | link budget, 4 scenarios (Ku downlink, S band, Ka wideband, L band) | the closed form link equation, rewritten in Python | 5.0e-13 dB |
| `atmosphere/check_gas.py` | 气体吸收,18 个频率,涵盖 60 GHz 氧气复体、22/183 GHz 水汽线 | `itur` 的 ITU-R P.676-12 实现 | 干空气 6.1e-11,水汽 6.4e-9(相对) | | `atmosphere/check_gas.py` | gaseous absorption, 18 frequencies covering the 60 GHz oxygen complex and the 22 and 183 GHz water lines | the ITU-R P.676-12 implementation in `itur` | dry air 6.1e-11, water 6.4e-9, relative |
| `atmosphere/check_rain.py` | 雨衰系数,17 个频率 × 4 组仰角/极化 | `itur` 的 ITU-R P.838-3 实现 | k 7.5e-9,alpha 6.5e-13(相对) | | `atmosphere/check_rain.py` | rain coefficients, 17 frequencies at 4 elevation and polarisation pairs | the ITU-R P.838-3 implementation in `itur` | k 7.5e-9, alpha 6.5e-13, relative |
| `atmosphere/check_rain_path.py` | 雨衰斜路径,8 个场景 | ITU-R P.618-13 方程(Python 转写,系数来自 `itur`) | 3.4e-13 km / 4.3e-13 dB | | `atmosphere/check_rain_path.py` | rain on a slant path, 8 scenarios | the ITU-R P.618-13 equations, transcribed to Python with coefficients from `itur` | 3.4e-13 km and 4.3e-13 dB |
| `link/check_s465.py` | 参考天线方向图,3 种口径 × 8 个离轴角 | ITU-R S.465 方程(Python 转写) | 4.0e-10 dB | | `link/check_s465.py` | reference antenna pattern, 3 apertures at 8 off-axis angles | the ITU-R S.465 equations, transcribed to Python | 4.0e-10 dB |
| `atmosphere/check_p839.py` | 零度等温线高度,15 个全球点 | `itur` 的 ITU-R P.839-4 实现 | 3.3e-13 km | | `atmosphere/check_p839.py` | zero degree isotherm height, 15 points worldwide | the ITU-R P.839-4 implementation in `itur` | 3.3e-13 km |
| `atmosphere/check_gas_exact.py` | 精确斜路径,11 个场景(1–100 GHz,1°–90° 仰角) | `itur` 的 ITU-R P.676-12 exact 方法 | 9.6e-9(相对) | | `atmosphere/check_gas_exact.py` | exact slant path, 11 scenarios from 1 to 100 GHz at 1 to 90 degrees elevation | the exact method of ITU-R P.676-12 in `itur` | 9.6e-9, relative |
| `time/check_iau2000.py` | 岁差、章动、框架偏移、GAST、GCRS→ITRF,8 个历元 × 28 个量 | ERFA 2.0.1(IAU SOFA 派生) | 章动 6.6e-15 rad;BPN 矩阵 6.0e-15;GAST 1.1e-11 rad | | `time/check_iau2000.py` | precession, nutation, frame offsets, GAST and GCRS to ITRF, 8 epochs by 28 quantities | ERFA 2.0.1, derived from IAU SOFA | nutation 6.6e-15 rad, BPN matrix 6.0e-15, GAST 1.1e-11 rad |
| `coord/check_teme.py` | TEME → 地固系,4 个历元 | ERFA 的 `gmst82` 与 `pom00`(同为 0 极移) | 4.9e-10 km | | `coord/check_teme.py` | TEME to the Earth-fixed frame, 4 epochs | `gmst82` and `pom00` from ERFA, both with zero polar motion | 4.9e-10 km |
| `constellation/check_coverage.py` | 覆盖几何,6 个场景(LEO/MEO/GEO × 低/常规/严格遮蔽角);以及地心角与仰角的往返 | 球面三角关系(Python 从几何独立重写) | 角度 0,面积相对 2.2e-16 | | `constellation/check_coverage.py` | coverage geometry, 6 scenarios of LEO, MEO and GEO at low, usual and strict masks, plus a round trip between the central angle and the elevation | the spherical triangle relations, rewritten in Python from the geometry | angle 0, area 2.2e-16 relative |
| `orbit/check_kepler.py` | 二体转换,7 个轨道(圆/低偏心/中偏心/高偏心 e=0.97/逆行/近极/地球同步);以及 36 个真近点角的状态反解 | 近日点公式 + 旋转矩阵 + 活力公式(Python 独立重写) | 位置、速度、周期 0;能量 3.0e-14 km²/s²;反解要素 ~1e-11 km | | `orbit/check_kepler.py` | two-body conversion over 7 orbits (circular, low, moderate and high eccentricity, retrograde, near polar and geostationary), plus a state back to elements at 36 true anomalies | the perifocal formulae, the rotation matrix and the vis-viva equation, rewritten in Python | position, velocity and period 0; energy 3.0e-14 km^2/s^2; recovered elements about 1e-11 km |
| `orbit/check_tle.py` | TLE 字段解析,8 个元素集 × 17 字段(含两数字年展开、隐含前导零偏心率、指数记法拖动项、模十校验和) | 按列索引的 NORAD 格式独立解析器 | 逐字段精确一致 | | `orbit/check_tle.py` | element parsing, 8 element sets by 17 fields, covering the two digit year expansion, the implied leading zero of the eccentricity, the exponential drag term and the modulo ten checksum | an independent column indexed reader of the NORAD format | exact, field by field |
### 关于射频层对拍的说明 ### A note on the radio frequency cross-checks
射频层分两类:有权威参考实现的模型与纯闭式算式。 The radio frequency layer divides into two kinds of model: those with an
authoritative reference implementation, and those that are purely a closed
form expression.
- 有参考实现的,直接对拍:地球定向用 `erfa`(IAU SOFA 的派生实现),大气与雨衰用 `itur`; - Where a reference implementation exists it is compared directly: Earth
链路预算的每一步都是初等运算,`check_link.py` 用 Python 独立重写同一方程。 orientation uses `erfa`, derived from IAU SOFA, and the atmosphere and rain
- 其余以解析性质断言为主:各 crate 的单元测试验证模型必须满足的性质,例如 3 dB 点、 use `itur`. Every step of the link budget is elementary arithmetic, and
平方律下降、Friis 级联行为、P.838-3 系数在表列频率处的取值。 `check_link.py` rewrites the same equation independently in Python.
- The rest are checked by asserting analytic properties: the unit tests of
each crate verify what the model must satisfy, such as the 3 dB point, the
square law falloff, the behaviour of a Friis cascade, and the value of the
P.838-3 coefficients at the tabulated frequencies.
所有建议书均采用**现行版本**:P.676-12、P.838-3、P.618-13、P.839-4、S.465。 Every recommendation is at its **current revision**: P.676-12, P.838-3,
P.838-3 的系数是闭式曲线拟合,不是已被取代的 P.838-1 数值表。 P.618-13, P.839-4 and S.465. The coefficients of P.838-3 are a closed form
curve fit, not the superseded numerical table of P.838-1.
### 关于 SGP4 对拍的约定 ### A note on the SGP4 cross-check convention
TLE 的历元是 UTC 日历时刻,但 SGP4 把它当作均匀时间原点使用,这是理论拟合时 The epoch of a TLE is a UTC calendar instant, but SGP4 treats it as a uniform
采用的约定。若先把历元换算到 TAI 再送入传播器,会引入约 33 秒的闰秒偏移,该偏移 time origin, which is the convention the theory was fitted with. Converting
会传播到深空分支的日月摄动级数中。对拍时两侧都直接使用 TLE 写明的 UTC MJD。 the epoch to TAI before handing it to the propagator would introduce a leap
second offset of about 33 seconds, and that offset would propagate into the
luni-solar series of the deep space branch. Both sides of the cross-check
therefore use the UTC Modified Julian Date as the element set states it.
### 关于时间对拍的约定差异 ### A note on the difference in the time cross-check conventions
UTC 的浮点日表示存在两种约定: There are two conventions for a floating point day number of UTC:
- 本仓库使用线性 MJD,`TAI - UTC` 是阶跃函数,闰秒日当天其余时刻偏移不变。 - This repository uses a linear Modified Julian Date, where `TAI - UTC` is a
- astropy / ERFA 使用准儒略日,闰秒日有 86401 秒,其 `tai - utc` 差值在 step function and the offset is constant for the rest of a leap second
该日内线性变化。 day.
- astropy and ERFA use a quasi-Julian date, where a leap second day has
86401 seconds and their `tai - utc` difference varies through the day.
因此直接比较两者的儒略日差会测量到“约定差异”而非数值误差。`check_time.py` Comparing the two Julian dates directly would therefore measure a difference
改比较 **TAI 瞬时**:两侧给定同一 UTC 日历时刻,再比较得到的 TAI 修正儒略日。 of convention rather than a numerical error. `check_time.py` compares the
比较采用“整数日 + 日内小数”两段式,避免单个 `f64` 在 MJD ≈ 60000 处约 **TAI instant** instead: both sides take the same UTC calendar instant and
40 µs 的分辨率损失。 the resulting TAI Modified Julian Date is compared. The comparison uses an
integer day plus a fraction of a day in two parts, which avoids the roughly
40 us of resolution a single `f64` loses near an MJD of 60000.
`dump_time` 也因此以两段式输出 TAI 时刻,而不是单个浮点日。 `dump_time` writes the TAI instant in the same two parts for the same
reason.

201
validation/README.zh.md Normal file
View File

@ -0,0 +1,201 @@
# 对拍验证
[English](./README.md) | 简体中文
本目录用于 Rust 实现与公开参考实现的数值对拍。Python 侧仅作验证用途,
不属于 Rust 依赖,也不参与 Rust 编译。
## Python 环境
使用 [pixi](https://pixi.sh/) 管理:
```bash
cd validation
pixi install
```
已允许的库:
- `sgp4`:SGP4 传播对拍
- `astropy`:时间、坐标对拍
- `numpy`:数值计算
- `skyfield`:轨道、可见性对拍(可选)
- `itur`:ITU-R 建议书参考实现(P.676、P.838、P.618),用于大气与雨衰对拍
引入新的 Python 库前需要评估并确认。
## 目录
```text
validation/
├── pixi.toml
├── python/
│ ├── orbit/ # SGP4、开普勒对拍
│ ├── time/ # 时间尺度、儒略日对拍
│ ├── coord/ # 坐标变换对拍
│ ├── link/ # 链路预算对拍
│ └── constellation/ # 覆盖几何对拍
└── data/
└── expected/ # Rust 侧导出的待比对数据
```
## 流程
Rust 侧通过 `apsis-rs` 的 example 导出数据,输出为 CSV(手写格式化,不引 serde):
```bash
# 大地坐标 -> ECEF
cargo run -p apsis-rs --features physics --example dump_geodesy \
> validation/data/expected/rust_geodetic.csv
# 时间尺度换算
cargo run -p apsis-rs --features physics --example dump_time \
> validation/data/expected/rust_time.csv
# SGP4/SDP4 传播
cargo run -p apsis-rs --features orbit --example dump_orbit \
> validation/data/expected/rust_sgp4.csv
# 链路预算
cargo run -p apsis-rs --features rf --example dump_link \
> validation/data/expected/rust_link.csv
# ITU-R P.676-12 气体吸收
cargo run -p apsis-rs --features rf --example dump_gas \
> validation/data/expected/rust_gas.csv
# ITU-R P.838-3 雨衰系数
cargo run -p apsis-rs --features rf --example dump_rain \
> validation/data/expected/rust_rain.csv
# ITU-R P.618-13 雨衰斜路径
cargo run -p apsis-rs --features rf --example dump_rain_path \
> validation/data/expected/rust_rain_path.csv
# ITU-R S.465 参考天线方向图
cargo run -p apsis-rs --features rf --example dump_s465 \
> validation/data/expected/rust_s465.csv
# ITU-R P.839-4 零度等温线高度
cargo run -p apsis-rs --features rf --example dump_p839 \
> validation/data/expected/rust_p839.csv
# ITU-R P.676-12 精确斜路径(分层积分 + 折射率射线追踪)
cargo run -p apsis-rs --features rf --example dump_gas_exact \
> validation/data/expected/rust_gas_exact.csv
# IAU 2000/2006 地球定向(岁差、章动、框架偏移、GCRS→ITRF)
cargo run -p apsis-rs --features physics --example dump_iau2000 \
> validation/data/expected/rust_iau2000.csv
# TEME → 地固系(SGP4 输出链路)
cargo run -p apsis-rs --features physics --example dump_teme \
> validation/data/expected/rust_teme.csv
# 覆盖几何(仰角、星下点角、地心角、斜距、覆盖面积)
cargo run -p apsis-rs --features network --example dump_coverage \
> validation/data/expected/rust_coverage.csv
# 二体模型(开普勒要素↔状态矢量,往返转换)
cargo run -p apsis-rs --features orbit --example dump_kepler \
> validation/data/expected/rust_kepler.csv
# 两线元素集解析(字段、历元展开、模十校验和)
cargo run -p apsis-rs --features orbit --example dump_tle \
> validation/data/expected/rust_tle.csv
```
Python 侧读取同一批输入,用参考实现重算并输出误差表:
```bash
pixi run python validation/python/coord/check_geodetic.py
pixi run python validation/python/time/check_time.py
pixi run python validation/python/orbit/check_sgp4.py
pixi run python validation/python/link/check_link.py
pixi run python validation/python/atmosphere/check_gas.py
pixi run python validation/python/atmosphere/check_rain.py
pixi run python validation/python/atmosphere/check_rain_path.py
pixi run python validation/python/link/check_s465.py
pixi run python validation/python/atmosphere/check_p839.py
pixi run python validation/python/atmosphere/check_gas_exact.py
pixi run python validation/python/time/check_iau2000.py
pixi run python validation/python/coord/check_teme.py
pixi run python validation/python/constellation/check_coverage.py
pixi run python validation/python/orbit/check_kepler.py
pixi run python validation/python/orbit/check_tle.py
```
两个脚本都以退出码 0 表示通过、1 表示超差、2 表示缺少输入文件。
## 容差
| 量 | 容差 |
| --- | --- |
| 位置、距离 | < 1e-3 km(SGP4 传播)或 1e-9 km(几何量) |
| 速度 | < 1e-6 km/s |
| 时间 | < 1e-6 s |
| 角度(覆盖几何) | 1e-12 rad |
| 面积、覆盖占比 | 相对 1e-12 |
| 链路预算、衰减 | 1e-6 至 1e-9 dB,视模型而定 |
| 地球定向矩阵 | 1e-9 |
各脚本把容差写成其源文件顶部的具名常量,值按所比较的物理量设定,见
`docs/design/errors.md` 的"对拍容差"一节。
超差时修正 Rust 侧算法,不放宽容差,除非经确认。
脚本必须可独立离线运行,不访问网络。`check_time.py` 会关闭 astropy 的
Earth orientation 数据自动下载;本 crate 涉及的换算不需要该数据。
## 已实现的检查
| 脚本 | 对比内容 | 参考实现 | 当前最差误差 |
| --- | --- | --- | --- |
| `coord/check_geodetic.py` | `Geodetic` → ECEF,280 个点,覆盖全球纬度、经度与高度 | `astropy.coordinates.EarthLocation` | 1.1e-8 m |
| `time/check_time.py` | UTC → TAI,117 个时刻,覆盖全部闰秒边界 | `astropy.time.Time` | 5.3e-7 s |
| `orbit/check_sgp4.py` | SGP4/SDP4 传播,4 组 TLE(近地、高偏心、深空、GTO)× 9 个时刻 | `sgp4` 参考库 | 位置 1.2e-8 km,速度 5.3e-12 km/s |
| `link/check_link.py` | 链路预算,4 个场景(Ku 下行、S 波段、Ka 宽带、L 波段) | 链路方程闭式解(Python 手写) | 5.0e-13 dB |
| `atmosphere/check_gas.py` | 气体吸收,18 个频率,涵盖 60 GHz 氧气复体、22/183 GHz 水汽线 | `itur` 的 ITU-R P.676-12 实现 | 干空气 6.1e-11,水汽 6.4e-9(相对) |
| `atmosphere/check_rain.py` | 雨衰系数,17 个频率 × 4 组仰角/极化 | `itur` 的 ITU-R P.838-3 实现 | k 7.5e-9,alpha 6.5e-13(相对) |
| `atmosphere/check_rain_path.py` | 雨衰斜路径,8 个场景 | ITU-R P.618-13 方程(Python 转写,系数来自 `itur`) | 3.4e-13 km / 4.3e-13 dB |
| `link/check_s465.py` | 参考天线方向图,3 种口径 × 8 个离轴角 | ITU-R S.465 方程(Python 转写) | 4.0e-10 dB |
| `atmosphere/check_p839.py` | 零度等温线高度,15 个全球点 | `itur` 的 ITU-R P.839-4 实现 | 3.3e-13 km |
| `atmosphere/check_gas_exact.py` | 精确斜路径,11 个场景(1–100 GHz,1°–90° 仰角) | `itur` 的 ITU-R P.676-12 exact 方法 | 9.6e-9(相对) |
| `time/check_iau2000.py` | 岁差、章动、框架偏移、GAST、GCRS→ITRF,8 个历元 × 28 个量 | ERFA 2.0.1(IAU SOFA 派生) | 章动 6.6e-15 rad;BPN 矩阵 6.0e-15;GAST 1.1e-11 rad |
| `coord/check_teme.py` | TEME → 地固系,4 个历元 | ERFA 的 `gmst82` 与 `pom00`(同为 0 极移) | 4.9e-10 km |
| `constellation/check_coverage.py` | 覆盖几何,6 个场景(LEO/MEO/GEO × 低/常规/严格遮蔽角);以及地心角与仰角的往返 | 球面三角关系(Python 从几何独立重写) | 角度 0,面积相对 2.2e-16 |
| `orbit/check_kepler.py` | 二体转换,7 个轨道(圆/低偏心/中偏心/高偏心 e=0.97/逆行/近极/地球同步);以及 36 个真近点角的状态反解 | 近日点公式 + 旋转矩阵 + 活力公式(Python 独立重写) | 位置、速度、周期 0;能量 3.0e-14 km²/s²;反解要素 ~1e-11 km |
| `orbit/check_tle.py` | TLE 字段解析,8 个元素集 × 17 字段(含两数字年展开、隐含前导零偏心率、指数记法拖动项、模十校验和) | 按列索引的 NORAD 格式独立解析器 | 逐字段精确一致 |
### 关于射频层对拍的说明
射频层分两类:有权威参考实现的模型与纯闭式算式。
- 有参考实现的,直接对拍:地球定向用 `erfa`(IAU SOFA 的派生实现),大气与雨衰用 `itur`;
链路预算的每一步都是初等运算,`check_link.py` 用 Python 独立重写同一方程。
- 其余以解析性质断言为主:各 crate 的单元测试验证模型必须满足的性质,例如 3 dB 点、
平方律下降、Friis 级联行为、P.838-3 系数在表列频率处的取值。
所有建议书均采用**现行版本**:P.676-12、P.838-3、P.618-13、P.839-4、S.465。
P.838-3 的系数是闭式曲线拟合,不是已被取代的 P.838-1 数值表。
### 关于 SGP4 对拍的约定
TLE 的历元是 UTC 日历时刻,但 SGP4 把它当作均匀时间原点使用,这是理论拟合时
采用的约定。若先把历元换算到 TAI 再送入传播器,会引入约 33 秒的闰秒偏移,该偏移
会传播到深空分支的日月摄动级数中。对拍时两侧都直接使用 TLE 写明的 UTC MJD。
### 关于时间对拍的约定差异
UTC 的浮点日表示存在两种约定:
- 本仓库使用线性 MJD,`TAI - UTC` 是阶跃函数,闰秒日当天其余时刻偏移不变。
- astropy / ERFA 使用准儒略日,闰秒日有 86401 秒,其 `tai - utc` 差值在
该日内线性变化。
因此直接比较两者的儒略日差会测量到“约定差异”而非数值误差。`check_time.py`
改比较 **TAI 瞬时**:两侧给定同一 UTC 日历时刻,再比较得到的 TAI 修正儒略日。
比较采用“整数日 + 日内小数”两段式,避免单个 `f64` 在 MJD ≈ 60000 处约
40 µs 的分辨率损失。
`dump_time` 也因此以两段式输出 TAI 时刻,而不是单个浮点日。