diff --git a/README.md b/README.md index 7067d23..c5e97ee 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,131 @@ -# ApsisRS / 拱点 +# ApsisRS -商业航天与卫星互联网领域的 Rust 计算内核。 +Computational kernels for commercial spaceflight and satellite internet, in Rust. -## 定位 +English | [简体中文](./README.zh.md) -- 纯 Rust、`std`、零第三方 crate 依赖 -- 领域边界清晰,按层单向依赖 -- 对外门面 crate 名为 `apsis-rs`,库名为 `apsis` -- 当前阶段所有 crate 均 `publish = false`,不执行发布 +## Scope -## 分层 +- 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 foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs ``` -约束: +The rules: -- 依赖只能从左向右。下层可依赖其上任意层,上层不得依赖其下任意层。 - 例如 `network` 可以依赖 `orbit`(向左),但 `orbit` 不得依赖 `network` - (向右,即反向)。左向依赖不限层数,允许跨层,因为把 `orbit` 的值 - 传到 `network` 时中间层未必提供中转类型。 -- `foundation` 的 `apsis-core` 是全局基准,任何层任何 crate 均可依赖它。 - `apsis-math` 同理,因为线性代数被各层共用。 -- 层内以该层的门面 crate(`apsis-orbit`、`apsis-rf`、`apsis-sim`)作为整合点。 +- Dependencies flow to the left only. A lower layer may use any layer to its + left; a higher layer may not use any layer to its right. So `network` may + use `orbit`, but `orbit` may not use `network`. A leftward dependency may + cross as many layers as it needs, because the layers in between need not + offer a type that carries the value forward. +- `apsis-core` in `foundation` is the global baseline that any crate in any + 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. -各层内容: - -| 层 | 职责 | crates | +| Layer | Responsibility | 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` | +| foundation | Base types, units, mathematics, macros | `apsis-core`, `apsis-units`, `apsis-math`, `apsis-macros` | +| physics | Time, leap seconds, coordinates, datum | `apsis-time`, `apsis-leap`, `apsis-coord`, `apsis-geodesy`, `apsis-frames` | +| orbit | TLE, SGP4, two-body, ephemeris, maneuvers | `apsis-tle`, `apsis-sgp4`, `apsis-kepler`, `apsis-ephemeris`, `apsis-maneuver`, `apsis-orbit` | +| rf | Radio frequency, antennas, link budget | `apsis-rf`, `apsis-antenna`, `apsis-doppler`, `apsis-atmosphere`, `apsis-noise`, `apsis-interference`, `apsis-link` | +| network | Constellations, coverage, access, topology, routing | `apsis-constellation`, `apsis-coverage`, `apsis-access`, `apsis-topology`, `apsis-isl`, `apsis-routing`, `apsis-handover`, `apsis-mesh` | +| ops | Resources, tasks, scheduling, conflicts, schedules | `apsis-resource`, `apsis-task`, `apsis-scheduler`, `apsis-conflict`, `apsis-schedule` | +| sim | Simulation engine, scenarios, events, output | `apsis-sim-engine`, `apsis-scenario`, `apsis-event`, `apsis-output`, `apsis-sim` | +| facade | Public facade | `apsis-rs` | -## 构建 +## Building -所有 Rust 命令在仓库根目录执行: +Run every Rust command from the repository root: ```bash cargo check --workspace 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 [dependencies] apsis-rs = { path = "...", features = ["orbit"] } ``` -可选 feature:`foundation`、`physics`、`orbit`、`rf`、`network`、`ops`、`sim`、`full`。 -每个层 feature 自动启用其下层。`full` 启用全部层(含 `sim`);`ops` 等价于除 -`sim` 以外的所有层,适合不运行仿真的项目。 +The features are `foundation`, `physics`, `orbit`, `rf`, `network`, `ops`, +`sim` and `full`. A layer feature enables the layers below it. `full` enables +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` -- 默认禁止任何第三方 crate,包括开发依赖与 `build.rs` -- 库代码不使用 `unwrap()` / `expect()` / `panic!` / `unsafe` - - 外部输入的失败一律走 `Result` / `Option`,不用 `unwrap` / `expect` 转换 - - 允许对**已守卫的内部不变量**用 `expect("…")` 断言不可能的分支(如刚 `push` - 后的 `last()`、前置 `is_none()` 已检查后的取值)。这些断言不处理不可信 - 输入,而是把"若此处为空则是编码错误"写成可读的失败信息;全库共 12 处, - 逐处均有前置守卫。 -- 公共 API 必须有文档注释,注释自包含 +- Toolchain `stable`, edition 2024, rust-version 1.85 +- No third-party crate by default, including dev-dependencies and build scripts +- Library code uses no `unwrap`, `expect` or `panic`. A failure on external + input is always a `Result` or an `Option`. An `expect` is allowed on an + internal invariant that a check just above already guards, such as the + element just pushed onto a vector, or a value whose absence the line above + ruled out. There are twelve such sites and each is listed with its guard in + `docs/design/errors.md`. +- 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 apsis-rs/ ├── Cargo.toml -├── crates/ # Rust 工作区成员 +├── crates/ # Rust workspace members │ └── apsis-rs/ -│ ├── examples/ # 对拍导出与跨层集成检查 +│ ├── examples/ # validation dumps and cross-layer checks │ └── ... -├── scripts/ # 仓库检查脚本 -├── validation/ # Python 对拍验证 -└── docs/design/ # 设计文档 +├── scripts/ # repository check scripts +├── validation/ # Python cross-checks +└── docs/design/ # design notes ``` -测试在各 crate 的 `tests/` 目录,为集成测试,只通过公共 API 访问被测 crate。 -跨层集成检查与零依赖基准测试是 `apsis-rs` 的 example: +Tests live in each crate's `tests/` directory. The cross-layer integration +checks and the dependency-free benchmarks are examples of the facade: ```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 ops --example integrate_ops 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 -# 规模扫描:输入规模递增,观察每单位工作量的增长趋势 +# 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 -# 并行扫描:同一批工作量在 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 ``` -## 许可 +## 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 + +GPL-3.0-or-later, see `LICENSE`. diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..3593743 --- /dev/null +++ b/README.zh.md @@ -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 +``` + +## 许可 + +GPL-3.0-or-later,见 `LICENSE`。 diff --git a/docs/design/README.md b/docs/design/README.md index f218cef..71a0e32 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -1,17 +1,22 @@ -# 设计文档 +# Design notes -本目录记录 ApsisRS 的跨 crate 设计决策,例如坐标约定、单位约定、误差策略与 -分层边界。单个 crate 的内部设计写在对应 crate 的模块文档中。 +English | [简体中文](./README.zh.md) -文档内容需自包含,仅引用本仓库内的代码或可公开访问的标准。 +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) -- [错误与容差策略](errors.md) -- [分层边界](layers.md) -- [并行化](parallel.md) +## Cross-layer conventions -## 各层设计 +- [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) diff --git a/docs/design/README.zh.md b/docs/design/README.zh.md new file mode 100644 index 0000000..41c598d --- /dev/null +++ b/docs/design/README.zh.md @@ -0,0 +1,19 @@ +# 设计文档 + +[English](./README.md) | 简体中文 + +本目录记录 ApsisRS 的跨 crate 设计决策,例如坐标约定、单位约定、误差策略与 +分层边界。单个 crate 的内部设计写在对应 crate 的模块文档中。 + +文档内容需自包含,仅引用本仓库内的代码或可公开访问的标准。 + +## 跨层约定 + +- [单位与坐标约定](units.md) +- [错误与容差策略](errors.md) +- [分层边界](layers.md) +- [并行化](parallel.md) + +## 各层设计 + +- [仿真层](sim.md) diff --git a/docs/design/errors.md b/docs/design/errors.md index 2605c6a..13ff5d3 100644 --- a/docs/design/errors.md +++ b/docs/design/errors.md @@ -1,19 +1,23 @@ -# 错误与容差策略 +# Errors and tolerances -本文件记录跨 crate 的错误表达与数值容差约定。单个 crate 的错误枚举见其 -`error.rs` 的文档。 +English | [简体中文](./errors.zh.md) -## 错误表达 +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 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum FooErrorKind { - /// 每个变体描述一个调用方可能想要区分的具体条件。 + /// Each variant names a concrete condition a caller may want to tell apart. ReversedInterval, DuplicateResource, // ... @@ -32,119 +36,148 @@ impl FooError { 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 {} ``` -约定: +The conventions: -- **`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` 作为公共错误。** 前者无法匹配, - 后者丢失类别。私有辅助函数内部可以传 `String` 作为原因文本,但公开 - 边界必须是枚举。`apsis-scheduler` 的 `precedence_floor` 是这种内部用法 - 的实例:它私有的 `String` 最终填入公开的 `TaskState::Infeasible { reason }`。 +- **An `ErrorKind` is marked `#[non_exhaustive]`**, so that adding a failure + category does not break a match downstream. All fifteen `ErrorKind` + enumerations carry the attribute. +- **An error enum that carries data is marked too**, including one whose + variants wrap a lower error, such as `apsis-orbit`'s + `EphemerisSamplingError`. +- **A state enum that is not an error is not marked.** A domain state such as + `TaskState`, `LinkState`, `TimeScale`, `HandoverDecision` or the antenna + `Pattern` gains from being matchable exhaustively inside a single + repository: when a variant is added, the compiler points at every `match` + that needs updating. The reasoning is the opposite of an error's, because + one more error category should let a caller fall through to a default + 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 + branches: if so, mark it; if not, leave it. +- **`kind` is matched, `detail` is read.** A caller matches on `kind` to + write recovery logic and logs `detail`. Because `detail` takes no part in + matching, its wording may change freely. +- **`Display` prints the kind**, followed by `": detail"` when there is one, + 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` 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` 是通用兜底类型,携带粗粒度类别 -(`InvalidInput`、`NotFound` 等)。它**不是**各层的统一错误表面:没有任何 -层使用它,因为通用类别丢失了调用方需要的区分(`InvalidInput` 无法区分 -"区间反向"与"重复标识")。 +`apsis-core::Error` and `ErrorKind` are a general purpose fallback carrying +a coarse category such as `InvalidInput` or `NotFound`. They are **not** the +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`** 表示"没有值"是正常结果,不是错误。例如某个时刻在覆盖区间 - 之外(`Ephemeris::state_at` 返回 `None`)、某个角度的正切无定义。 -- **`Result`** 表示操作因可诊断的原因失败。例如区间反向、资源重复、 - 超出模型定义域。 +- **`Option`** means that having no value is an ordinary outcome rather than + a failure, such as an instant outside the covered interval, which + `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`; -若需要区分失败原因并据此恢复,用 `Result`。 +The test is whether the caller needs to know **why**. If it only needs to +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-sgp4` | `KEPLER_TOLERANCE` | `1e-12` | 开普勒方程迭代残差 | +| `apsis-kepler` | `TOLERANCE` | `1e-14` | the step of the Newton iteration for the eccentric anomaly | +| `apsis-sgp4` | `KEPLER_TOLERANCE` | `1e-12` | the residual of the Kepler equation iteration | -这些值在浮点精度与迭代次数之间折中:取到接近 `f64` 的机器精度,同时 -保证迭代在有限步内收敛(各有对应的 `MAX_ITERATIONS`)。 +These values trade floating point precision against iteration count: they sit +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 | -| 速度 | < 1e-6 km/s | -| 时间 | < 1e-6 s | -| 覆盖几何角度 | 1e-12 rad | -| 覆盖面积、占比 | 相对 1e-12 | +| Position | < 1e-3 km | +| Velocity | < 1e-6 km/s | +| Time | < 1e-6 s | +| Coverage angle | 1e-12 rad | +| Coverage area and fraction | 1e-12 relative | -容差设在"远高于观测到的偏差(通常在浮点精度量级),远低于模型本身的 -精度"之间。这样既能捕获半径写错、单位漏换算这类错误,又不会因最后一位 -浮点差异而误报。详见 `validation/README.md` 的容差表。 +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 +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 -不需要容差时应避免引入:整数索引、计数、布尔标志用精确相等;角度归约 -(`wrap_to_pi`)在边界上用半开区间,不引入 epsilon。浮点比较仅在量本身 -是连续近似时使用。 +A tolerance should be avoided where it is not needed. An integer index, a +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!`。这条规则的**意图**是 -不允许用它们处理外部输入的失败——那种失败必须走 `Result` 或 `Option`,让 -调用方能够处理。 +The workspace convention is that library code uses no `unwrap`, `expect` or +`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`(更差)或写出冗长的手工分支。 -因此允许一种例外:**对已由前置检查守卫的内部不变量使用 -`expect("说明")`**。这类断言不把不可信数据转成 panic,而是把"若此处为空, -则是本 crate 的编码错误"写成一条可读的失败信息。 +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: +**an `expect` on an internal invariant that a check has already guarded.** +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. -判据:能否在紧邻的前一行找到使该分支不可达的守卫,或该值是否由本函数 -刚刚构造。当前 12 处全部满足,逐处为: +The test is whether a guard making the branch unreachable sits on the line +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-scheduler/improve.rs` | 刚检查 `is_none()` 的值非空 | 上一行 `is_none()` 分支 | -| `apsis-scheduler/planner.rs` | order 中的 id 在 task 列表里 | order 由 task 列表构造 | -| `apsis-topology/series.rs` | 时间有限 | 构造时已校验 | -| `apsis-handover/decision.rs` | 候选列表非空 | 上一行 `validate` | -| `apsis-constellation/walker.rs` | 平面数 / 每面星数为正 | 构造时已校验除数 | -| `apsis-kepler/elements.rs` | node / 半径非零 | 紧邻的 `norm() == 0.0` 分支 | -| `apsis-event/queue.rs` | 出堆项必有 payload | 队列构造时序保证 | +| `apsis-math/src/parallel.rs` | every slot was filled by one thread | the loop assigns each index exactly once | +| `apsis-schedule/src/schedule.rs` | the placement just pushed exists | the line above | +| `apsis-scheduler/src/improve.rs` | the value whose `is_none()` was just checked is not none | the `is_none()` branch above | +| `apsis-scheduler/src/planner.rs` | an identifier in the order is in the task list | the order is built from the task list | +| `apsis-topology/src/series.rs` | the time is finite | validated on construction | +| `apsis-handover/src/decision.rs` | the candidate list is not empty | the `validate` call above | +| `apsis-constellation/src/walker.rs` | the plane count and the satellites per plane are positive | the divisor is validated before | +| `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`,包括构造失败与恢复路径。 -- 对拍脚本以退出码表达结果:0 通过、1 超差、2 缺输入。 -- `cargo clippy -- -D warnings` 保证没有 `unwrap`/`expect` 漏进库代码 - (工作区约定库代码不使用它们)。 +- Each crate's tests cover every `kind` of its error, including the path + that constructs it and the path that recovers from it. +- 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. diff --git a/docs/design/errors.zh.md b/docs/design/errors.zh.md new file mode 100644 index 0000000..f2f8942 --- /dev/null +++ b/docs/design/errors.zh.md @@ -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, +} + +impl FooError { + pub fn new(kind: FooErrorKind) -> Self; + pub fn with_detail(self, detail: impl Into) -> 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` 作为公共错误。** 前者无法匹配, + 后者丢失类别。私有辅助函数内部可以传 `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` 漏进库代码 + (工作区约定库代码不使用它们)。 diff --git a/docs/design/layers.md b/docs/design/layers.md index d10d84f..14e22e2 100644 --- a/docs/design/layers.md +++ b/docs/design/layers.md @@ -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 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 -index(B) <= index(A) 允许 -index(B) > index(A) 违规(反向依赖) +index(B) <= index(A) allowed +index(B) > index(A) a violation, a reverse dependency ``` -即**只能向左依赖,不能向右依赖**。`scripts/check-layers.sh` 对工作区每个 -清单执行这条判定,CI 会运行它。 +In other words, **dependencies go to the left and never to the right.** +`scripts/check-layers.sh` applies this to every manifest in the workspace, +and CI runs it. -## 允许跨层 +## Crossing layers is allowed -规则只看方向,不看距离。从 `network` 直接依赖 `orbit` 是允许的,尽管中间 -隔着 `rf`:`orbit` 在 `network` 左边,方向正确。 +The rule constrains direction, not distance. `network` may depend on `orbit` +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` 层并 - 非必然提供一个承载该位置的类型。强行要求经过 `rf` 只会制造只是转发的 - 空壳类型。 -- **无谓的耦合。** `network` 的星座计算需要 `apsis-kepler` 的开普勒要素, - 它并不需要 `rf`。若禁止跨层,`network` 就得依赖 `rf` 只为拿到一个它不 - 使用的类型。 +- **There may be no type to carry the value.** When a position from the + `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 + produce a shell type that forwards. +- **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`、 -`Mat3`、`Quat`)被各层共用。它们本身在 `foundation` 中,所以"只向左依赖" -已经允许任何层使用它们,无需特例。 +`apsis-core` (the general error, the constants, the unit marker) and +`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` 可以被任何 -crate 使用,但一个不引用它们的 crate 不应把它们写进清单:未使用的依赖会 -拖慢编译、让 `cargo tree` 与分层检查产生噪声,并让"谁依赖什么"失真。 -需要时再加,是一条随时可走的单行改动。`scripts/check-layers.sh` 报告的 -边数因此是真实的使用关系,而不是一份预留清单。 +**Declare a dependency only where it is used.** Any crate may use +`apsis-core` and `apsis-math`, but a crate that does not reference them +should not list them: an unused dependency slows the build, adds noise to +`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` | | rf | `apsis-rf` | | sim | `apsis-sim` | -| 对外 | `apsis-rs` | +| outward | `apsis-rs` | -门面把同层的能力组合成少量类型,并再导出同层的其余 crate。`foundation`、 -`physics`、`network`、`ops` 没有单独门面:它们的 crate 各自独立有用,且 -`apsis-rs` 会把它们一并再导出。 +A facade combines the capabilities of its layer into a few types and +re-exports the remaining crates of the same layer. `foundation`, `physics`, +`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` 不在层序列中。** 它依赖全部层,因为它就是出口。 - `scripts/check-layers.sh` 通过层目录名判定,`crates/apsis-rs` 不是 - `crates//` 形状,因此被跳过。 -- **`dev-dependencies` 也计入检查。** 一条测试里的反向依赖同样说明分层 - 错了,应当修正代码而不是放宽测试。 -- **门面再导出不构成新依赖。** `apsis-rs` 把 `apsis-orbit` 再导出,是 - 门面行为,不是某层对 `orbit` 的依赖。 +- **The `apsis-rs` facade is not in the layer sequence.** It depends on + every layer, because it is the exit. `scripts/check-layers.sh` decides by + the layer directory name, and `crates/apsis-rs` does not have the shape + `crates//`, so it is skipped. +- **Dev-dependencies count.** A reverse dependency in a test shows the + layering is just as wrong there, so the fix is to change the code rather + 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 scripts/check-layers.sh ``` -脚本输出检查过的依赖边数,并在发现反向依赖时逐条列出后以退出码 1 结束。 -对当前工作区,它检查 104 条边,全部方向正确。 +The script prints the number of dependency edges it checked, and lists each +reverse dependency before exiting with status one if it found any. diff --git a/docs/design/layers.zh.md b/docs/design/layers.zh.md new file mode 100644 index 0000000..c7e0726 --- /dev/null +++ b/docs/design/layers.zh.md @@ -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//` 形状,因此被跳过。 +- **`dev-dependencies` 也计入检查。** 一条测试里的反向依赖同样说明分层 + 错了,应当修正代码而不是放宽测试。 +- **门面再导出不构成新依赖。** `apsis-rs` 把 `apsis-orbit` 再导出,是 + 门面行为,不是某层对 `orbit` 的依赖。 + +## 检查 + +```bash +bash scripts/check-layers.sh +``` + +脚本输出检查过的依赖边数,并在发现反向依赖时逐条列出后以退出码 1 结束。 +边数随代码变化,因此此处不写死;运行脚本得到的数字才是当前的真实关系。 diff --git a/docs/design/parallel.md b/docs/design/parallel.md index e860b02..4c2166d 100644 --- a/docs/design/parallel.md +++ b/docs/design/parallel.md @@ -1,121 +1,159 @@ -# 并行化 +# Parallelism -本文件记录并行化的判定标准、提供的接口,以及实测结果。跨层的单位与坐标约定见 -[单位与坐标约定](units.md),分层边界见 [分层边界](layers.md)。 +English | [简体中文](./parallel.zh.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` 这样的线程池可用。也不需要: -工作区中值得多核的运算,无一例外都是**逐元素独立**的循环,用标准库的 -`std::thread::scope` 就能直接表达。 +## Why there is no thread pool -作用域线程按引用借用数据,调用方无需 `Arc` 即可共享一个切片或一个只读对象; -作用域结束前会 join 全部线程,因此不会有结果在线程仍在写时被读走。 +The workspace forbids every third-party crate, so a pool such as `rayon` is +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()` | 机器的并行度 | -| `threads_for(len)` | 该长度下值得启用的线程数 | -| `for_each_index(len, f)` | 对每个下标调用 `f` | -| `map_index(len, f)` | 每个下标产出一个值,按下标序返回 | +| `thread_count()` | the parallelism of the machine | +| `threads_for(len)` | the threads worth starting for a length | +| `for_each_index(len, f)` | call `f` for every index | +| `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_at_parallel` | 整壳一次性传播 | -| `apsis-access` | `find_all_parallel` | 数百条轨道 | -| `apsis-isl` | `visibility_matrix_parallel` | 数百颗以上(见下) | +| `apsis-sgp4` | `propagate_minutes_at_parallel` | a long track, tens of thousands of samples or more | +| `apsis-sgp4` | `propagate_at_parallel` | a whole shell in one call | +| `apsis-access` | `find_all_parallel` | hundreds of tracks | +| `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**: -- **值得并行**:单次工作昂贵(≥ 微秒),且逐元素独立,规模可达数百次以上。 - 典型是 SGP4 传播(约 230 ns/样本,百万样本量级)与可见性搜索(约 7 μs/轨)。 -- **不值得并行**: - - `Constellation::instantiate`:纯算术,几百颗星只需微秒,线程开销完全淹没。 - - 调度器:贪心放置本质串行,每次放置改变后续任务的可行区间,不是独立 - 循环,因此没有并行接口;但其中的搜索已改为单次扫描(见下)。 - - 事件引擎:单队列时序,无法分块。 - - 大地与坐标变换:单点调用本身已是 O(1),没有批量。 - - `mutual_visibility` 单次:约 4 ns,**单次**不值得,但成矩阵(见下)。 +- **Worth threading**: the work per item is expensive, at a microsecond or + more, the items are independent, and the batch reaches hundreds. The + typical cases are SGP4 propagation at about 230 ns a sample over millions + 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. + - The scheduler: a greedy placement is serial in substance, since each + 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× | -| 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× | +| 1 day (1,440 samples) | 0.3 ms | 0.3 ms | 1.28x | +| 7 days (10,080) | 2.3 ms | 0.9 ms | 2.46x | +| 30 days (43,200) | 10.1 ms | 1.9 ms | 5.45x | +| 365 days (525,600) | 144.1 ms | 26.0 ms | 5.54x | -**结论**:轨道越长加速越高,30 天以上稳定在 5.5×。一天以内收益很小—— -1,440 个样本分给多线程后每线程不足一千项,低于 `threads_for` 的阈值。 +**Conclusion**: the longer the track the larger the gain, settling near 5.5x +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× | -| 600 | 36 万 | 1.50 ms | 1.37 ms | 1.09× | -| 1,000 | 100 万 | 4.09 ms | 2.50 ms | 1.64× | +| 200 | 40 thousand | 0.16 ms | 0.31 ms | 0.53x | +| 600 | 360 thousand | 1.50 ms | 1.37 ms | 1.09x | +| 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 的状态、可见性 -的布尔矩阵、access 的过站摘要),顺序也一致。传播本身是确定性的,只依赖时刻, -与如何分块无关;可见性与过站搜索是逐元素独立的。因此同一输入在任意线程数下 -产生相同输出,测试也按此断言。 +Threading does not change the result. Every batch interface compares its +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. -**第一步:一次扫描代替逐候选测试。** 旧的做法列出所有"可行性可能变化"的时刻, -逐个测试,每次都重新计算每个资源的负载曲线。负载曲线与候选时刻无关,因此改为 -扫一次:把"资源没有余量的区间"从可用窗口中减去,多资源求交,取第一个存活区间 -的起点。结果仍由 `feasible_at` 确认,枚举法保留为浮点边界不一致时的回退。 +**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 +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 -秒(14 倍),6400 个任务 2.2 秒。计划结果不变——测试在两种资源、窗口不相交的 -场景下逐个比对了每个放置的起止时刻。 +**Result**: with every task contending for one resource and the capacity the +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. diff --git a/docs/design/parallel.zh.md b/docs/design/parallel.zh.md new file mode 100644 index 0000000..38e1cc8 --- /dev/null +++ b/docs/design/parallel.zh.md @@ -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 秒。计划结果不变——测试在两种资源、窗口不相交的 +场景下逐个比对了每个放置的起止时刻。 diff --git a/docs/design/sim.md b/docs/design/sim.md index ee6c820..6eb43ef 100644 --- a/docs/design/sim.md +++ b/docs/design/sim.md @@ -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 -apsis-event 事件类型与事件队列 -apsis-sim-engine 离散事件引擎 -apsis-scenario 场景定义与文本加载 -apsis-output 仿真输出记录 -apsis-sim 仿真门面(整合上述四个) +apsis-event the event type and the event queue +apsis-sim-engine the discrete event engine +apsis-scenario scenario definition and text loading +apsis-output simulation output recording +apsis-sim the facade, which joins the four above ``` -依赖方向: +The dependency direction: ```text apsis-event -> apsis-sim-engine -> apsis-sim -> apsis-rs @@ -20,66 +23,87 @@ apsis-scenario -----------------> apsis-sim apsis-output -------------------> apsis-sim ``` -`apsis-sim` 同时依赖 ops 层(`apsis-scheduler`、`apsis-schedule`、 -`apsis-task`、`apsis-resource`),因为它要规划场景并读取计划。其余 crate -只依赖 foundation。 +`apsis-sim` also depends on the ops layer (`apsis-scheduler`, +`apsis-schedule`, `apsis-task`, `apsis-resource`), because it plans a +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` -在投递前加上 `epoch`。若不偏移,引擎会在投递首个事件时把时钟回拨。 +The `epoch` of a scenario is where the clock starts. The times in a timeline +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` -的数量。达到上限后再次 `run` 不执行任何事件。这样"上限"始终表示一个不会 -被绕过的界,调用方要运行剩余事件需显式提高上限。 +The event limit in `apsis-sim-engine` bounds the total for the life of the +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` 自带一套零依赖的行式文本格式,用于存储与重新加载。 -解析分三遍(资源、任务、优先级约束),因此 `require` 可以引用文件中稍后 -定义的资源,`precedence` 可以引用任意顺序定义的任务。每处失败都带行号。 +`apsis-scenario` carries its own line oriented text format with no +dependency, for storing a scenario and loading it back. Parsing runs in three +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: -- 事件队列表述的同刻顺序规则; -- 规划器对同权任务的稳定排序(见 ops 层); -- 记录器保留插入顺序,`Trace::in_time_order` 为稳定排序。 +- the same-instant ordering rule of the event queue; +- the stable ordering the planner applies to tasks of equal weight, described + 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. diff --git a/docs/design/sim.zh.md b/docs/design/sim.zh.md new file mode 100644 index 0000000..075457c --- /dev/null +++ b/docs/design/sim.zh.md @@ -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` 示例断言同一次准备出的仿真重复运行得到相同轨迹。 diff --git a/docs/design/units.md b/docs/design/units.md index bde7ce8..ff91997 100644 --- a/docs/design/units.md +++ b/docs/design/units.md @@ -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-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-kepler`, `apsis-sgp4`, `apsis-tle` | kilometres | +| `apsis-ephemeris` | kilometres | +| `apsis-maneuver` | kilometres | +| `apsis-constellation`, `apsis-coverage`, `apsis-access`, `apsis-isl` | kilometres | +| `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` | no length; `quality` is defined by the caller and time is in seconds | +| `apsis-coord` (`Eci`, `Ecef`) | metres | +| `apsis-geodesy` (geodetic height, ellipsoid semi-axes, Cartesian position) | metres | +| `apsis-frames` | dimensionless (angles and rotations) | +| `apsis-rf`, `apsis-antenna`, `apsis-atmosphere`, `apsis-noise`, `apsis-doppler`, `apsis-interference`, `apsis-link` | metres, hertz | -选择千米还是米并非任意:卫星轨道用千米表达更自然(`apsis-sgp4` 输出的量级 -是几千),射频链路用米表达更自然(波长与距离用米,与 ITU-R 建议书一致)。 -代价是边界处必须显式换算。 +The choice is not arbitrary. Satellite orbits read naturally in kilometres, +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 -// orbit 层的千米位置转 physics 层的米位置。 -let position_km = satellite.propagate_minutes(0.0)?.position; // 千米 -let ecef = Ecef::new(position_km * 1_000.0); // 米 +// A position in kilometres from the orbit layer becomes metres in physics. +let position_km = satellite.propagate_minutes(0.0)?.position; // kilometres +let ecef = Ecef::new(position_km * 1_000.0); // metres ``` -漏掉这一步会把卫星放到地球内部,而且数值仍然"看起来合理",不会立刻报错。 -`integrate_network` 示例对这一边界做了断言:换算后的半径必须与传播器报告 -的半径一致,这是唯一能自动发现漏乘或漏除的检查。 +Omitting this step puts the satellite inside the Earth, and the numbers still +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 格式 -用度与每日圈数,因为那是文件的编码。`apsis-sgp4` 在初始化时换算为弧度与 -弧度每分钟。 +The only exception is the two-line element set, whose raw fields are in +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)起算的秒数。 -- orbit 层:`apsis-sgp4` 用"距元素历元的分钟数",`apsis-ephemeris` 与 - 其余 crate 用秒。 -- 日历时间:`apsis-time` 的 `Epoch` 存 TAI 儒略日;UTC 换算经 `apsis-leap` - 的闰秒表。 +- The ops and sim layers count seconds from the scenario epoch. +- The orbit layer uses minutes from the element set epoch in `apsis-sgp4`; + `apsis-ephemeris` and the rest use seconds. +- 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 定义的真赤道平春分点框架 | -| ECI | `apsis_coord::Eci` | 惯性笛卡尔坐标 | -| ECEF | `apsis_coord::Ecef` | 地固笛卡尔坐标 | -| 大地坐标 | `apsis_geodesy::Geodetic` | 纬度、经度、大地高 | -| 地面点 | `apsis_coverage::GroundPoint` | 大地坐标加仰角遮蔽 | +| TEME | `apsis_sgp4::State` | the true equator, mean equinox frame the SGP4 model defines | +| ECI | `apsis_coord::Eci` | inertial Cartesian coordinates | +| ECEF | `apsis_coord::Ecef` | Earth-fixed Cartesian coordinates | +| Geodetic | `apsis_geodesy::Geodetic` | latitude, longitude, height above the ellipsoid | +| Ground point | `apsis_coverage::GroundPoint` | a geodetic position plus an elevation mask | -TEME 到 ECEF 需要地球自转角(`apsis-frames::EarthRotationAngle`)加上岁差 -章动;`apsis-coord` 提供变换,`apsis-frames` 提供角。TEME 与 ECI 的差别是 -分点方程,量级低于千分之一度;`integrate_network` 示例中直接把它当作惯性 -系处理,并注明这是该示例的近似。 +Going from TEME to ECEF needs the Earth rotation angle +(`apsis-frames::EarthRotationAngle`) together with precession and nutation. +`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 对应 - SGP4,WGS 84 对应其余用途;选错会使密切半长轴偏约百万分之一。门面 - `apsis-orbit::Orbit` 把引力参数与传播器绑定,避免这一错误。 -- 速度:与长度同层,km/s 或 m/s。 -- 功率用瓦、频率用赫兹、温度用开尔文;分贝为功率比,`dBi` 相对各向同性、 - `dBd` 相对半波偶极子(相差 2.15 dB)。 -- 相对量一律无量纲:偏心率、填充率、利用率、信噪比的分贝值。 +- The gravitational parameter is `apsis-kepler::GravityParameter`, in + cubic kilometres per second squared. WGS 72 goes with SGP4 and WGS 84 with + everything else; choosing the wrong one shifts the osculating semi-major + axis by about one part in a million. The `apsis-orbit::Orbit` facade binds + the parameter to the propagator so that this cannot go wrong. +- 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: -- 各层测试覆盖层内换算。 -- `integrate_network` 断言千米与米的边界换算保持半径。 -- `integrate_ops`、`integrate_sim` 覆盖 ops 与 sim 层的时间轴约定。 -- `cargo run --release -p apsis-rs --features full --example bench` 覆盖 - 各层热点路径,任一层的接口变更导致编译失败都会在此暴露。 +- Each layer's tests cover the conversions inside that layer. +- `integrate_network` asserts that the kilometre to metre boundary conversion + preserves the radius. +- `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. diff --git a/docs/design/units.zh.md b/docs/design/units.zh.md new file mode 100644 index 0000000..67a914d --- /dev/null +++ b/docs/design/units.zh.md @@ -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` 覆盖 + 各层热点路径,任一层的接口变更导致编译失败都会在此暴露。 diff --git a/validation/README.md b/validation/README.md index 3f397a7..beae8d5 100644 --- a/validation/README.md +++ b/validation/README.md @@ -1,109 +1,118 @@ -# 对拍验证 +# Cross-check validation -本目录用于 Rust 实现与公开参考实现的数值对拍。Python 侧仅作验证用途, -不属于 Rust 依赖,也不参与 Rust 编译。 +English | [简体中文](./README.zh.md) -## 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 cd validation pixi install ``` -已允许的库: +The libraries that are allowed: -- `sgp4`:SGP4 传播对拍 -- `astropy`:时间、坐标对拍 -- `numpy`:数值计算 -- `skyfield`:轨道、可见性对拍(可选) -- `itur`:ITU-R 建议书参考实现(P.676、P.838、P.618),用于大气与雨衰对拍 +- `sgp4`: the SGP4 propagation cross-check +- `astropy`: the time and coordinate cross-checks +- `numpy`: numerical computation +- `skyfield`: the orbit and visibility cross-checks, optional +- `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 validation/ ├── pixi.toml ├── python/ -│ ├── orbit/ # SGP4、开普勒对拍 -│ ├── time/ # 时间尺度、儒略日对拍 -│ ├── coord/ # 坐标变换对拍 -│ ├── link/ # 链路预算对拍 -│ └── constellation/ # 覆盖几何对拍 +│ ├── orbit/ # SGP4 and two-body cross-checks +│ ├── time/ # time scale and Julian date cross-checks +│ ├── coord/ # coordinate transform cross-checks +│ ├── link/ # link budget cross-checks +│ └── constellation/ # coverage geometry cross-checks └── 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 -# 大地坐标 -> ECEF +# Geodetic -> ECEF cargo run -p apsis-rs --features physics --example dump_geodesy \ > validation/data/expected/rust_geodetic.csv -# 时间尺度换算 +# Time scale conversions cargo run -p apsis-rs --features physics --example dump_time \ > validation/data/expected/rust_time.csv -# SGP4/SDP4 传播 +# SGP4 and SDP4 propagation cargo run -p apsis-rs --features orbit --example dump_orbit \ > validation/data/expected/rust_sgp4.csv -# 链路预算 +# Link budget cargo run -p apsis-rs --features rf --example dump_link \ > 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 \ > 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 \ > 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 \ > 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 \ > 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 \ > 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 \ > 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 \ > 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 \ > 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 \ > 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 \ > 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 \ > 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 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 ``` -两个脚本都以退出码 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(几何量) | -| 速度 | < 1e-6 km/s | -| 时间 | < 1e-6 s | -| 角度(覆盖几何) | 1e-12 rad | -| 面积、覆盖占比 | 相对 1e-12 | -| 链路预算、衰减 | 1e-6 至 1e-9 dB,视模型而定 | -| 地球定向矩阵 | 1e-9 | +| Position and distance | < 1e-3 km for SGP4 propagation, 1e-9 km for a geometric quantity | +| Velocity | < 1e-6 km/s | +| Time | < 1e-6 s | +| Coverage angle | 1e-12 rad | +| Area and coverage fraction | 1e-12 relative | +| Link budget and attenuation | 1e-6 to 1e-9 dB, depending on the model | +| Earth orientation matrix | 1e-9 | -各脚本把容差写成其源文件顶部的具名常量,值按所比较的物理量设定,见 -`docs/design/errors.md` 的"对拍容差"一节。 +Each script names its tolerance as a constant at the top of its source, set +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 的 -Earth orientation 数据自动下载;本 crate 涉及的换算不需要该数据。 +Every script must run offline on its own and reach no network. +`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 | -| `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 格式独立解析器 | 逐字段精确一致 | +| `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 to TAI, 117 instants covering every leap second boundary | `astropy.time.Time` | 5.3e-7 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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 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` | 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` | 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` | 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`; - 链路预算的每一步都是初等运算,`check_link.py` 用 Python 独立重写同一方程。 -- 其余以解析性质断言为主:各 crate 的单元测试验证模型必须满足的性质,例如 3 dB 点、 - 平方律下降、Friis 级联行为、P.838-3 系数在表列频率处的取值。 +- Where a reference implementation exists it is compared directly: Earth + orientation uses `erfa`, derived from IAU SOFA, and the atmosphere and rain + use `itur`. Every step of the link budget is elementary arithmetic, and + `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。 -P.838-3 的系数是闭式曲线拟合,不是已被取代的 P.838-1 数值表。 +Every recommendation is at its **current revision**: P.676-12, P.838-3, +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 把它当作均匀时间原点使用,这是理论拟合时 -采用的约定。若先把历元换算到 TAI 再送入传播器,会引入约 33 秒的闰秒偏移,该偏移 -会传播到深空分支的日月摄动级数中。对拍时两侧都直接使用 TLE 写明的 UTC MJD。 +The epoch of a TLE is a UTC calendar instant, but SGP4 treats it as a uniform +time origin, which is the convention the theory was fitted with. Converting +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` 是阶跃函数,闰秒日当天其余时刻偏移不变。 -- astropy / ERFA 使用准儒略日,闰秒日有 86401 秒,其 `tai - utc` 差值在 - 该日内线性变化。 +- This repository uses a linear Modified Julian Date, where `TAI - UTC` is a + 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` -改比较 **TAI 瞬时**:两侧给定同一 UTC 日历时刻,再比较得到的 TAI 修正儒略日。 -比较采用“整数日 + 日内小数”两段式,避免单个 `f64` 在 MJD ≈ 60000 处约 -40 µs 的分辨率损失。 +Comparing the two Julian dates directly would therefore measure a difference +of convention rather than a numerical error. `check_time.py` compares the +**TAI instant** instead: both sides take the same UTC calendar instant and +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. diff --git a/validation/README.zh.md b/validation/README.zh.md new file mode 100644 index 0000000..ca7bf47 --- /dev/null +++ b/validation/README.zh.md @@ -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 时刻,而不是单个浮点日。