#geometry #algebra #affine #science

affn

Affine geometry primitives: strongly-typed coordinate systems, reference frames, and centers for scientific computing

10 releases (5 breaking)

new 0.6.0 Mar 31, 2026
0.5.0 Mar 27, 2026
0.4.1 Mar 8, 2026
0.3.3 Feb 26, 2026
0.1.0 Dec 21, 2025

#146 in Math

Download history 17/week @ 2026-01-13 1/week @ 2026-01-20 87/week @ 2026-02-17 25/week @ 2026-02-24 45/week @ 2026-03-03 11/week @ 2026-03-10 20/week @ 2026-03-24

76 downloads per month
Used in 2 crates

AGPL-3.0-only

340KB
6K SLoC

affn

Crates.io Docs Code Quality

Affine geometry primitives for strongly-typed coordinate systems.

affn is a small, domain-agnostic geometry kernel for scientific and engineering software. It provides:

  • Reference centers (C): the origin of a coordinate system (optionally parameterized via C::Params)
  • Reference frames (F): the orientation of axes
  • Typed coordinates: Cartesian, spherical, and ellipsoidal positions plus directions/vectors
  • Conic geometry: domain-agnostic conic-family classification plus shape/orientation containers
  • Affine operators: Rotation3, Translation3, and Isometry3
  • Units: distances/lengths are carried via qtty units at the type level

The goal is to make invalid operations (like adding two positions) fail at compile time.

Quick Start

Add the dependency:

[dependencies]
affn = "0.4"
qtty = "0.4"

Define a center + frame and do basic affine algebra:

use affn::cartesian::{Displacement, Position};
use affn::centers::ReferenceCenter;
use affn::frames::ReferenceFrame;
use qtty::*;

#[derive(Debug, Copy, Clone)]
struct World;
impl ReferenceFrame for World {
    fn frame_name() -> &'static str { "World" }
}

#[derive(Debug, Copy, Clone)]
struct Origin;
impl ReferenceCenter for Origin {
    type Params = ();
    fn center_name() -> &'static str { "Origin" }
}

let a = Position::<Origin, World, Meter>::new(0.0, 0.0, 0.0);
let b = Position::<Origin, World, Meter>::new(3.0, 4.0, 0.0);

// Position - Position -> Displacement
let d: Displacement<World, Meter> = b - a;
assert!((d.magnitude().value() - 5.0).abs() < 1e-12);

// Position + Displacement -> Position
let c = a + d;
assert!((c.y().value() - 4.0).abs() < 1e-12);

Core Concepts

  • Position<C, F, U>: an affine point (depends on both center and frame)
  • Direction<F>: a unit vector (frame-only, translation-invariant)
  • Vector<F, U> / Displacement<F, U> / Velocity<F, U>: free vectors (frame-only)
  • EllipsoidalPosition<C, F, U>: geodetic longitude/latitude/height on an ellipsoid-aware frame
  • PeriapsisParam<U> / SemiMajorAxisParam<U>: conic shapes classified by eccentricity
  • OrientedConic<S, F>: oriented conic section generic over shape and reference frame

The type system enforces the usual affine rules:

  • Position - Position -> Displacement
  • Position + Displacement -> Position
  • Position + Position (does not compile)

Conic Geometry

affn::conic is the geometry-only conic layer. It models shape and orientation without introducing body, epoch, or propagation semantics.

  • PeriapsisParam<U> is the erased shape form that works for elliptic, parabolic, and hyperbolic conics.
  • SemiMajorAxisParam<U> is the erased non-parabolic form, so e == 1 is rejected at construction.
  • classify() turns erased shapes into typed wrappers such as TypedPeriapsisParam<U, Elliptic>.
  • ConicOrientation<F> and OrientedConic<S, F> keep the orientation tagged with the reference frame.
use affn::conic::{
    ClassifiedPeriapsisParam, ConicKind, ConicOrientation, ConicShape, OrientedConic,
    PeriapsisParam,
};
use affn::frames::ReferenceFrame;
use qtty::*;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Inertial;

impl ReferenceFrame for Inertial {
    fn frame_name() -> &'static str { "Inertial" }
}

let shape = PeriapsisParam::try_new(7000.0 * M, 0.42).expect("valid elliptic periapsis shape");
assert_eq!(shape.kind(), ConicKind::Elliptic);

let ClassifiedPeriapsisParam::Elliptic(typed) = shape.classify() else {
    unreachable!("0.42 is elliptic")
};
let orientation = ConicOrientation::<Inertial>::try_new(28.5 * DEG, 40.0 * DEG, 15.0 * DEG)
    .expect("valid conic orientation");
let conic = OrientedConic::new(typed, orientation);
let sma = conic.to_semi_major_axis().expect("elliptic conics convert to SMA");

assert_eq!(sma.kind(), ConicKind::Elliptic);
assert_eq!(sma.orientation(), conic.orientation());

For a fuller walkthrough, see the conic_showcase example:

  • cargo run --example conic_showcase

Feature Flags

  • serde: serialization support for public coordinate and conic types
  • astro: astronomy-oriented marker types and integrations
  • ffi: enables repr(transparent) on thin wrapper types that have one real storage field plus marker PhantomData

Affine Operators

affn includes typed affine operators for pure geometric transforms:

  • Rotation3
  • Translation3
  • Isometry3

These operators preserve the existing frame tag. When you intentionally rotate from one frame into another, retag the result explicitly with reinterpret_frame:

use affn::cartesian::Position;
use affn::ops::Rotation3;
use affn::prelude::*;
use qtty::*;

#[derive(Debug, Copy, Clone, ReferenceFrame)]
struct FrameA;

#[derive(Debug, Copy, Clone, ReferenceFrame)]
struct FrameB;

#[derive(Debug, Copy, Clone, ReferenceCenter)]
struct Center;

let pos_a = Position::<Center, FrameA, Meter>::new(1.0 * M, 0.0 * M, 0.0 * M);
let rot = Rotation3::rz(90.0 * DEG);
let pos_b = (rot * pos_a).reinterpret_frame::<FrameB>();

assert!(pos_b.y().value() > 0.999);

Defining Custom Systems (Derive Macros)

For zero-sized marker types, use the derive macros re-exported by the crate:

use affn::prelude::*;

#[derive(Debug, Copy, Clone, ReferenceFrame)]
struct MyFrame;

#[derive(Debug, Copy, Clone, ReferenceCenter)]
struct MyCenter;

Some centers need runtime parameters (e.g. “topocentric” depends on an observer):

use affn::prelude::*;

#[derive(Clone, Debug, Default, PartialEq)]
struct Observer {
    lat_deg: f64,
    lon_deg: f64,
}

#[derive(Debug, Copy, Clone, ReferenceCenter)]
#[center(params = Observer)]
struct Topocentric;

Spherical ↔ Cartesian

Cartesian and spherical positions can be converted losslessly (up to floating point error):

use affn::cartesian::Position as CPos;
use affn::spherical::Position as SPos;
use affn::centers::ReferenceCenter;
use affn::frames::ReferenceFrame;
use qtty::*;

#[derive(Debug, Copy, Clone)]
struct Frame;
impl ReferenceFrame for Frame { fn frame_name() -> &'static str { "Frame" } }

#[derive(Debug, Copy, Clone)]
struct Center;
impl ReferenceCenter for Center { type Params = (); fn center_name() -> &'static str { "Center" } }

let cart = CPos::<Center, Frame, Meter>::new(1.0, 1.0, 1.0);
let sph: SPos<Center, Frame, Meter> = cart.to_spherical();
let back: CPos<Center, Frame, Meter> = CPos::from_spherical(&sph);
assert!((back.z().value() - 1.0).abs() < 1e-10);

Ellipsoidal Coordinates

affn::ellipsoidal::Position<C, F, U> models geodetic longitude, latitude, and height above an ellipsoid. Conversions to and from Cartesian coordinates are available when the frame implements HasEllipsoid.

Built-in terrestrial frames under the astro feature already carry ellipsoids:

  • ECEF uses Wgs84
  • ITRF uses Grs80

Example:

use affn::centers::ReferenceCenter;
use affn::ellipsoidal::Position;
use qtty::*;

#[derive(Debug, Copy, Clone)]
struct Geocentric;
impl ReferenceCenter for Geocentric {
    type Params = ();
    fn center_name() -> &'static str { "Geocentric" }
}

# #[cfg(feature = "astro")]
# {
let geodetic = Position::<Geocentric, affn::frames::ECEF, Meter>::new(
    -17.89 * DEG,
    28.76 * DEG,
    2200.0 * M,
);
let cart = geodetic.to_cartesian::<Meter>();
let roundtrip = Position::<Geocentric, affn::frames::ECEF, Meter>::from_cartesian(&cart);
assert!((roundtrip.height.value() - geodetic.height.value()).abs() < 1e-6);
# }

Built-In Astro Frames (optional)

Enable the astro feature to use the built-in astronomy and geodesy marker frames:

[dependencies]
affn = { version = "0.4", features = ["astro"] }
qtty = "0.4"

Available built-ins include:

  • Equatorial: ICRS, ICRF, EquatorialMeanJ2000, EME2000, EquatorialMeanOfDate, EquatorialTrueOfDate, GCRS, CIRS, TIRS, FK4B1950, TEME
  • Ecliptic and galactic: EclipticMeanJ2000, EclipticOfDate, EclipticTrueOfDate, Galactic
  • Terrestrial and horizontal: Horizontal, ECEF, ITRF
  • Planetary body-fixed: MercuryFixed, VenusFixed, MarsFixed, MoonPrincipalAxes, JupiterSystemIII, SaturnFixed, UranusFixed, NeptuneFixed, PlutoFixed

Examples

Run the included examples:

  • cargo run --example basic_cartesian
  • cargo run --example conic_showcase
  • cargo run --example parameterized_center
  • cargo run --example spherical_roundtrip
  • cargo run --example serde_roundtrip --features serde

Serde (optional)

affn supports serde serialization for its core coordinate types behind an opt-in feature flag.

Enable it in your Cargo.toml:

[dependencies]
affn = { version = "0.4", features = ["serde"] }
qtty = "0.4"

This feature also forwards serialization support to dependencies where needed (e.g. qtty/serde, and nalgebra's serde-serialize).

To run the serde example:

  • cargo run --example serde_roundtrip --features serde

Or to run tests including serde round-trips:

  • cargo test --features serde

License

Licensed under AGPL-3.0-only. See LICENSE.

Dependencies

~4.5MB
~97K SLoC