cargo fmt --all cargo clippy --all-targets --all-features -- -D warnings cargo build --all-targets --all-features cargo test
Some people may assume:
- It’s primarily about type-level patterns, not about practical 3D math.
- Or that it’s an “academic toy crate” rather than production-oriented.
You can counter this with:
- Good docs and examples using aircraft/robot/sensor frames.
- Performance notes.
- CI + tests + fuzzing + “Safety & Correctness” section.
If tests are all green and you’re happy, next natural step is either:
README.mdthat mirrors the safety story and shows usage examples, or- starting to integrate a math backend (e.g.,
nalgebra) behind feature flags.
├── benches/
│ ├── transform_bench.rs
│ ├── point_bench.rs
Those files are for benchmarking — measuring performance — using a crate like Criterion, which you already added as a dev-dependency.
🧭 Why benchmarks exist in this crate
Because spatial-typestate is meant for real-time robotics / aerospace / control systems, you may eventually need to answer:
- How many
apply_point()calls per second can we do? - Is quaternion normalization fast enough?
- Does switching to nalgebra / glam improve performance?
- How does
no_stdmode change speed?
Benchmarks allow you to compare implementations and track regressions.
For example — if we start with a naïve matrix multiply, then later introduce SIMD or nalgebra — benchmarks will prove whether it really improved speed.
📦 How Rust Benchmark Layout Works
Benchmarks live in:
benches/
transform_bench.rs
point_bench.rsCargo automatically runs them with:
cargo bench…but only if you use Criterion (recommended).
The built-in Rust unstable #[bench] framework is nightly-only — so Criterion is preferred.
📐 Example Benchmark Files
benches/transform_bench.rs
use criterion::{criterion_group, criterion_main, Criterion};
use spatial_typestate::{spatial_frames, Frame, Point3, Transform};
spatial_frames! { World }
pub fn bench_apply_point(c: &mut Criterion) {
let t: Transform<World, World> = Transform::from_translation(10.0, 0.0, 0.0);
let p: Point3<World> = Point3::new(1.0, 2.0, 3.0);
c.bench_function("apply_point", |b| {
b.iter(|| {
let _ = t.apply_point(p);
})
});
}
criterion_group!(benches, bench_apply_point);
criterion_main!(benches);benches/point_bench.rs
use criterion::{criterion_group, criterion_main, Criterion};
use spatial_typestate::{spatial_frames, Frame, Point3};
spatial_frames! { World }
pub fn bench_point_new(c: &mut Criterion) {
c.bench_function("point_new", |b| {
b.iter(|| {
let _ = Point3::<World>::new(1.0, 2.0, 3.0);
})
});
}
criterion_group!(benches, bench_point_new);
criterion_main!(benches);🧪 Running the Benchmarks
cargo benchCriterion will:
- Compile optimized
--releasemode - Run thousands of iterations
- Produce text output + HTML reports (usually in
target/criterion/report/)
🚦 Benchmarking in a safety-critical mindset
Benchmarks here aren’t just about speed — they give you quantitative evidence like:
| Operation | µs/iter | ns/iter | Hz |
|---|---|---|---|
apply_point naïve |
90 ns | 0.09 µs | 11,000,000 ops/sec |
apply_point via nalgebra |
14 ns | 0.014 µs | 71,000,000 ops/sec |
This tells you whether a design change is worth it before taking on implementation risk.
📌 TL;DR — What are those files for?
✔ They measure performance, not correctness ✔ They inform design choices (matrix backend, SIMD, etc.) ✔ They are essential for high-rate real-time systems (flight dynamics, robotics, sensors)
🚀 Ready to add real benchmarking? If you want, I can:
- Fully generate both benchmark files
- Add a benchmark README
- Add a GitHub Action to run Criterion + upload HTML reports
Phase 0 – MVP:
-
Point3<F>,Vector3<F>with phantom frame type. -
Transform<From, To>withapply(Point3<From>) -> Point3<To>. - No
unsafe,no_stdcompatible. - Right-handed coord system + units clearly documented.
Phase 1 – Safety hardening:
- Newtypes for units (
Meters,Radians, etc.). -
UnitQuat<F>with smart constructors. - Property-based tests for transform invariants.
-
clippy+ zero warnings in CI.
Phase 2 – Safety-critical posture:
- Fuzz tests for transforms & composition.
- Optional NaN/Inf checks in debug builds.
- "Safety and Correctness" design doc in
docs/. - Map crate practices to generic safety-critical ideas (determinism, invariants, absence of UB).
- What should we call this?
- How to aim this at safety-critical use "Safety-critical direction" here means:
- API makes misuse hard or impossible
- Implementation is minimal, predictable, and verifiable
- Process & docs look like something an auditor could respect
Think in three layers:
A. API design for correctness
- Everything is tagged by frame and units
Point3<F>: position in frameFVector3<F>: vector in frameFTransform<From, To>: rigid transform fromFromtoTo- Optional:
Angle<Radians>,Angle<Degrees>,Length<Meters>, etc. No nakedf32/f64for anything with semantics.
- No “generic” transforms
Don’t expose a Transform that doesn’t know source/target frames. Always:
struct Transform<From, To> { ... }
impl<From, To> Transform<From, To> {
fn apply(&self, p: Point3<From>) -> Point3<To> { ... }
}This forbids “oops I passed a World point to a Body transform” at compile time.
- Invariants encoded in types Example: rotations must be normalized.
struct UnitQuat<F> { /* guaranteed unit length */ }
impl<F> UnitQuat<F> {
// Smart constructor that either normalizes or returns Err if too degenerate
fn try_new(x: f64, y: f64, z: f64, w: f64) -> Result<Self, QuaternionError>;
}You never expose a raw “maybe-unit” quaternion to the public API.
- Typestate for lifecycle / validity If you later add, say, calibration or alignment:
struct Sensor;
struct Uncalibrated;
struct Calibrated;
struct SensorFrame<State> { ... }
type UncalibratedSensorFrame = SensorFrame<Uncalibrated>;
type CalibratedSensorFrame = SensorFrame<Calibrated>;
impl SensorFrame<Uncalibrated> {
fn calibrate(self, data: CalibrationData) -> SensorFrame<Calibrated> { ... }
}Now you can’t accidentally use an uncalibrated frame where a calibrated one is required.
- Deliberately “no convenience foot-guns”
- No automatic
From<Point3<World>> for Point3<Body>. - No implicit unit conversions.
- Conversions always go through explicit APIs like
.to_frame(...)or.convert_units(...).
B. Implementation discipline (safety-critical flavor)
- No
unsafein the public crate (or isolate it in a tiny, audited module)
- Either: "no
unsafeat all" (best for trust). - Or: 1 small
unsafemodule with documented safety contracts and exhaustive tests.
- No panics in normal flow
- Public functions return
Result<_, Error>instead ofpanic!. - Panics only for programmer bugs (e.g.
unreachable!()in truly unreachable branches).
- Deterministic, side-effect-free core
- Pure math functions: no global state, no randomness, no time access.
- Same inputs → same outputs.
no_stdsupport For safety-critical / embedded / avionics-style work,no_stdis a big plus:
- Use
#![no_std]at the crate root, with optional std feature for convenience. - Avoid heap allocation in core types.
- Clippy and lint policy
- Turn on
#![deny(warnings)]in CI. - Use
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]and selectively allow what you really need. - This supports a “zero-warning” policy, which safety folks like.
C. Validation, testing, and documentation
- Property-based tests
Use
proptestorquickcheckstyle tests:
-
For random valid rotations and points:
T.inverse().apply(T.apply(p)) ≈ p(T2 ∘ T1).apply(p) ≈ T2.apply(T1.apply(p))
-
Test composition/inversion round-trip.
- Cross-checking with a reference library
- Internally validate your transforms against
nalgebra/cgmathin tests. - This gives a “second implementation” to catch mistakes.
- Numerical safety checks (behind a feature flag)
- In debug or with a
check-nansfeature:- Assert all outputs are finite (no NaN/inf).
- Provide clear docs about:
- coordinate conventions (right-handed? which axis up/forward?),
- units (meters, radians),
- expected ranges.
- Fuzzing
- Simple fuzz targets: random transforms, compositions, inverses.
- Check for:
- no panics,
- no NaNs (optional),
- invariants hold.
- Documentation as a “mini safety case”
In
README.mdanddocs/:
- State goals: prevent frame mixups via Rust’s type system.
- Explain invariants: frames, units, right-hand rule, normalization.
- Explain non-goals: e.g., “this does not replace independent verification and validation (IV&V), but supports it.”
- Add formal-ish language that safety people like: “misuse prevented by construction,” “typestate-encoded invariants,” etc.
- API stability & semver discipline
- Avoid breaking changes to core types casually.
- When possible, add new frames / types in a backward-compatible way.
- Maintain a clear
CHANGELOG.md.