# affn

[![Crates.io](https://img.shields.io/crates/v/affn.svg)](https://crates.io/crates/affn)
[![Docs](https://docs.rs/affn/badge.svg)](https://docs.rs/affn)
[![Code Quality](https://github.com/Siderust/affn/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Siderust/affn/actions/workflows/ci.yml)


Affine geometry primitives for strongly-typed coordinate systems.

`affn` is a domain-agnostic geometry **core** with an optional, feature-gated catalogue of astronomical reference frames (`astro` feature, opt-in). The core 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.

## Scope

`affn` is split into a domain-agnostic kernel and an optional astronomical catalogue:

- **Kernel (always available, domain-agnostic).** The `algebra`, `cartesian`, `spherical`, `ellipsoidal`, `conic`, `planar`, `ops`, `frames`, `centers`, and `ellipsoid` modules form the geometry core. They define the affine machinery, typed coordinate containers, conic shapes, and the `ReferenceFrame` / `ReferenceCenter` traits, without committing to any particular application domain.
- **Astronomical catalogue (`astro` feature, opt-in).** The `frames::astro` module ships ready-made marker frames such as `ICRS`, `ICRF`, `EME2000`, `GCRS`, `CIRS`, `Galactic`, `EclipticMeanJ2000`, `Horizontal`, `ECEF`, `ITRF`, and the planetary body-fixed frames. These are convenience definitions layered on top of the kernel.
- **Lean default.** The default build contains only the domain-agnostic geometry kernel. Enable `astro` when you want the astronomy/geodesy marker catalogue; otherwise define your own frames using the derive macros.
- **Why ship them in-crate?** Rust's orphan rules require the derive-generated inherent impls (e.g. `Direction::<ICRS>::new(ra, dec)`, frame-specific spherical constructors via `#[frame(inherent)]`) to live in the same crate as the types they extend (`Direction`, `Position`, …). Splitting the astro frames into a separate downstream crate would lose these ergonomic constructors, so they live behind a feature flag inside `affn` instead.

## Quick Start

Add the dependency:

```toml
[dependencies]
affn = "0.7.0"
qtty = "0.7.1"
```

Define a center + frame and do basic affine algebra:

```rust
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.

```rust
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

- `astro`: astronomy and geodesy marker frames, plus the corresponding `qtty/astro` units
- `serde`: serialization support for public coordinate and conic types; also enables `qtty/serde`

Thin wrapper types that have one real storage field plus marker `PhantomData` use transparent layouts directly; no extra feature flag is required.

## 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`:

```rust
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);
```

`Translation3<U>` and `Isometry3<U>` carry the translation unit in the type. `Translation3::new(...)` remains available for raw numeric code, while `Translation3::from_quantities([1.0 * KM, 0.0 * KM, 0.0 * KM])` infers `Translation3<Kilometer>` and only applies to coordinates in compatible units. Use `to_unit::<TargetUnit>()` when a translation needs explicit unit conversion.

Use `Rotation3::try_from_matrix(...)` for untrusted matrices. `Rotation3::from_matrix(...)` and `Rotation3::from_matrix_unchecked(...)` are unchecked compatibility constructors; their input must be a finite proper rotation matrix.

## Defining Custom Systems (Derive Macros)

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

```rust
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):

```rust
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):

```rust
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);
```

Frame-specific spherical constructors generated with `#[frame(inherent)]` canonicalize pole crossings without changing the represented direction. Raw constructors (`new_raw` / `new_unchecked`) are kept for compatibility and trusted data; they store angles exactly as supplied.

## 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:

```rust
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:

```toml
[dependencies]
affn = { version = "0.6.2", features = ["astro"] }
qtty = "0.7.0"
```

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`:

```toml
[dependencies]
affn = { version = "0.6¡7.0", features = ["serde"] }
qtty = "0.7.1"
```

This feature also forwards serialization support to dependencies where needed, such as `qtty/serde`.

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`.
