A small Rust library for frame-tagged 3D points, vectors, unit quaternions, and rigid transforms. APIs are evolving. The crate is not certified for safety-, life-, or mission-critical use; applications must independently validate their physical frame definitions, calibration, unit declarations, and numerical requirements.
As checked on 2026-11-09, Rust 0.1.0 and Python 0.1.0 are published. Rust requires 1.85 or newer. Add the registry dependency to Cargo.toml:
[dependencies]
spatial-typestate = "0.1.0"For a library dependency without std, use this instead:
spatial-typestate = { version = "0.1.0", default-features = false, features = ["no_std"] }For Python, use a virtual environment with GIL-enabled CPython >=3.11,<3.15:
python -m pip install --only-binary=:all: spatial-typestate==0.1.0Published abi3 wheels are cp311-abi3-win_amd64 and
cp311-abi3-manylinux_2_28_x86_64: Windows x86_64 and Linux x86_64 with glibc >=2.28.
Qualification exercised the same wheel bytes on CPython 3.11/3.14 and, for Linux,
glibc 2.28/2.36 containers. Python 3.12/3.13 are allowed by metadata, not in the
CI matrix; local Windows 3.12 is additional evidence. No macOS, musl, ARM, PyPy
or free-threaded support is claimed. Wheel-only installation fails if no compatible
wheel exists; omitting that option can select the sdist and require an installed
Rust toolchain and build tools. See the Python guide.
Both releases identify source commit 9974fd6a00521d631d6ff6e35d4aa936f6b624cf
(C2). Tags rust-v0.1.0 and python-v0.1.0 both point there and must never move.
See migration from Rust 0.0.1 and Python first-release limits.
Python 0.1.0 was token-uploaded without public PyPI attestations; its embedded
source record is not a signed attestation. All future PyPI uploads require
Trusted Publishing plus per-file PEP 740 attestations.
use spatial_typestate::{Dimensionless, Feet, Meters, Point3, Transform, UnitQuat, Vector3, spatial_frames};
spatial_frames! { Sensor, Aircraft, World }
fn main() -> Result<(), spatial_typestate::SpatialError> {
let sensor_aircraft = Transform::<Sensor, Aircraft, Meters>::try_from_rotation_translation(
UnitQuat::try_from_components(0.0, 0.0, 1.0, 1.0)?, // Rz(+90 degrees)
Vector3::<_, Meters>::new(1.0, 2.0, 3.0), // source origin in target coordinates
)?;
let aircraft_world = Transform::<Aircraft, World, Meters>::try_from_rotation_translation(
UnitQuat::try_from_components(1.0, 0.0, 0.0, 1.0)?, // Rx(+90 degrees)
Vector3::<_, Meters>::new(10.0, 20.0, 30.0),
)?;
let sensor_world = sensor_aircraft.then(aircraft_world)?;
let point = Point3::<Sensor, Meters>::new(2.0, 0.0, 1.0);
let world = sensor_world.apply_point(point)?;
assert!((world.x - 11.0).abs() < 1e-9);
assert!((world.y - 16.0).abs() < 1e-9);
assert!((world.z - 34.0).abs() < 1e-9);
let restored = sensor_world.try_inverse()?.apply_point(world)?;
assert!((restored.x - point.x).abs() < 1e-9);
let vector = sensor_world.apply_vector(Vector3::<_, Dimensionless>::new(2.0, 0.0, 1.0))?;
assert!((vector.y + 1.0).abs() < 1e-9); // translation is not applied
// Explicit conversion: both point and translation must use the same length.
let feet_map = sensor_world.convert_to::<Feet>()?;
let feet_point = point.convert_to::<Feet>()?;
let feet_world = feet_map.apply_point(feet_point)?.convert_to::<Meters>()?;
assert!((feet_world.z - world.z).abs() < 1e-9);
Ok(())
}Run the complete example, which verifies all coordinates, sequential/composed mapping, inverse roundtrip, and vectors:
cargo +1.99.0-x86_64-pc-windows-msvc run --locked --jobs 2 --example sensor_alignment
cargo +1.99.0-x86_64-pc-windows-msvc run --locked --jobs 2 --no-default-features --features no_std --example sensor_alignmentThe example executable uses std for printing even when the library is no_std. Its analytic result is point (11,16,34) and vector (0,-1,2). These are supplied numeric relationships, not a sensor calibration performed by the library.
- Bare frame structs only need
impl Frame; the four spatial value types have Copy/Clone/Debug/PartialEq without marker bounds. Frame mismatches still fail to compile. A label cannot prove that physical measurements use the right frame. - The checked transform constructor uses Hamilton (x,y,z,w), right-handed active algebra and column vectors: p_To=R(q)*p_From+t_To. Columns of R are source basis vectors in To; t is the source origin in To. UnitQuat alone does not encode the source label. Application axes and physical frame relationships remain caller choices.
first.then(next)means next(first(point)).apply_vectoruses only R. Row storage is matrix[row][column], with last-column translation. Raw bottom rows are ignored by point application/composition, and composition emits a canonical affine bottom row. Each operation returns Result and rejects non-finite participating inputs or outputs. Sequential and composed mappings check different intermediates, so their overflow errors can differ.- Public matrix storage/from_matrix remain unvalidated.
try_inversechecks finite entries, affine row, orthonormal rotation and det(R)=+1 with absolute tolerance 1e-10, then returns R^T and -R^T*t. It rejects non-rigid finite input or non-finite input/output, including inverse translation overflow. - Inverse tolerance permits near-rigid input. Error scales with coordinate and translation magnitude and accumulates over long chains; no universal absolute roundtrip bound exists. Successful application/composition returns finite values; raw construction does not establish a finite or rigid invariant.
- UnitQuat accepts all finite nonzero components, including MAX/subnormals, rejects non-finite/all-zero input, and has private fields with const getters. Unit length is approximate; tiny relative components may round to signed zero. Equality compares components, so q and -q are unequal despite equivalent rotation.
- Points/transforms require an explicit length tag; vectors require an explicit unit tag. There is no default unit. Translation, point mapping and composition require matching lengths. Generic vector mapping preserves its unit independently of translation. UnitQuat remains dimensionless.
- Explicit checked
convert_to::<Feet>()/convert_to::<Meters>()is shared with Quantity; it preserves frames and changes only transform translation. No automatic mixing, dimensional algebra or angle conversion is implemented. Raw constructors/public components still assert units without validating them. - SpatialError implements Display/core Error. Constructors return its concrete exhaustive enum without allocation/dynamic dispatch; callers may choose boxed propagation. New variants require downstream exhaustive-match updates.
See requirements and evidence, math conventions, design, frames, and safety notes.
Replace old Point3<F>, Vector3<F>, and Transform<From,To> annotations with
Point3<F,L>, Vector3<F,U>, and Transform<From,To,L>. Choose the actual units;
old numbers are not automatically meters. Use Dimensionless for direction vectors,
not positions or translation. Existing method names and checked Result handling
remain. Meters and international Feet are the first convertible length tags.
Custom Unit/LengthUnit markers remain open. Optional LinearLengthUnit metadata
provides meters-per-unit for length conversions only; opaque vectors can rotate
without metadata. Custom declarations are trusted, not dimensionally inferred.
Nonpositive/non-finite scales return InvalidUnitScale; a ratio rounded to zero
or infinity returns UnrepresentableUnitConversion. Add these variants to
exhaustive SpatialError matches. Non-finite inputs/results return NonFinite.
Conversion computes a representable scale ratio then multiplies components. Rounding and component underflow to signed zero are allowed; overflow is an error. Some extreme ratios are rejected even when rearranging arithmetic could work. Finite same-unit component bits are preserved. No exact roundtrip or universal accuracy guarantee is made. Raw matrix coefficients must be dimensionless for these unit semantics; tags cannot verify calibration, raw data, or custom claims. See unit decisions and all limitations for the conservative algorithm, raw escape hatches and deliberately excluded scope.
The only runtime features are std (default) and no_std (a marker).
Use the published registry dependency above for this guide's API.
Historical snapshot (2026-11-04): crates.io then had legacy 0.0.1 while the
checkout used unpublished 0.0.2. The following previously validated Git pin has
the demonstrated core API but manifest version 0.0.2, not release identity 0.1.0;
it is retained as history, not the recommended install route. Do not add a
0.1.0 version constraint to that historical pin:
spatial-typestate = { git = "https://github.com/TelemetryWorks/spatial-typestate.git", rev = "1b80c3d7aa930538eb29ce0107a6e5ec2523ea86", default-features = false, features = ["no_std"] }Enabling no_std alongside defaults does not disable std. All runtime feature graphs have zero normal dependencies; dev testing/benchmark tools remain. The unused nalgebra/glam features/dependencies were removed, not implemented as backends. The std path retains native sqrt; no-std uses a private eight-step Newton helper only on scaled squared norms in [1,4]. No libm, correctly-rounded sqrt equivalence or timing guarantee is claimed.
The declared MSRV is 1.85, the committed development pin is 1.91.1, and explicit local/CI runtime verification uses installed Rust 1.99.0. No toolchain policy was changed. CONTRIBUTING.md has exact commands and CI scope. The CI workflow runs Linux/Windows default/no_std/ all-feature tests, example execution, warning-denied lint/docs, compatibility library checks, dependency graph gates, and runner-only thumb/MUSL cross-builds. Local tests do not claim remote CI success or device execution. Bare-metal target installation is authorized on CI only; it has not been performed locally.
The translation compatibility helper has deterministic/property tests, not a cargo-fuzz setup. Two other example files remain placeholders; diagrams are unpopulated. See migration/change notes and the canonical remaining-work list. License: Apache-2.0.
Measured host study and limitations cover checked math/conversion, actual std/no-std normalization, error/extreme inputs, chained/batched workflows and isolated optimization controls. Results retain raw samples, per-run intervals and observed spikes on one Windows/Ryzen host across LTO/no-LTO profiles. They are not universal or embedded timing guarantees. No production optimization or new prepared/batch API is introduced by the study.
cargo +1.99.0-x86_64-pc-windows-msvc bench --locked --jobs 2 --bench spatial -- --test
# Run selected timings only in a quiet window; see the report for repeats/profiles.
cargo +1.99.0-x86_64-pc-windows-msvc bench --locked --jobs 2 --bench spatial -- point_candidatesBorrowed-validation prototype and decision record compare caller-local hoisting with lifetime-bound point/vector preparations. The isolated prototype preserves per-input/output checks and numeric/type contracts. Preparation error timing and any batch API remain decisions for review; no production API is added. New measurements remain separate from the original study, with environment load, variance and codegen limitations recorded.
The published, separate Python package provides Rust-owned point/vector batches, immutable runtime frame/unit metadata, checked transforms/quaternions, and bytes-backed memoryviews without a Python object per row. Python frame/unit errors are runtime checks, not Rust compile-time proofs. Import/export copy, the binding requires std/CPython, and custom Rust unit markers are not exposed. The core remains dependency-free/no_std-capable. Python 0.1.0 is available as described above; no native performance parity is implied.
The synthetic Python lab walkthrough uses the installed wheel for a 100 Hz sensor-aircraft-world batch analysis. The release/versioning policy defines compatibility and mandatory future publication/attestation gates.
Package readiness adds inspectable source/build records and license checks to backend-produced Python artifacts. Linux wheels target glibc >= 2.28 x86_64 with narrow container checks; this is not universal Linux support or publication approval.