# Errors and tolerances 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 ### 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, // ... } #[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", or "kind: detail" */ } impl core::error::Error for FooError {} ``` The conventions: - **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` 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`** 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. 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 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 | Constant | Value | Meaning | | --- | --- | --- | --- | | `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 | 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 | | --- | --- | | 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 | 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 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 in library code 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. 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. 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-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 | Removing a guard means the matching `expect` must become a `Result`; the two change together. ## Checks - 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.