astrodyn_quantities 0.2.0

Phantom-tagged typed quantities (Position, Velocity, ...) for orbital dynamics
Documentation
//! `Qty3<D, F>` — a frame-tagged, dimension-typed 3-vector.

use core::marker::PhantomData;

use glam::DVec3;
use uom::si::{Dimension, Quantity, SI};

use crate::derive_utils::{
    impl_clone_phantom, impl_copy_phantom, impl_debug_phantom, impl_partial_eq_phantom,
};
use crate::frame::Frame;

/// Componentwise 3-vector: each of `x`, `y`, `z` carries the dimension `D`,
/// and the whole vector carries the frame tag `F`.
///
/// Layout: `#[repr(C)]` with three contiguous `f64`s. `raw_si()` and
/// `from_raw_si` are layout-compatible with `DVec3`; see the `layout`
/// unit tests for the static size/align assertions.
#[repr(C)]
pub struct Qty3<D: ?Sized + Dimension, F: Frame> {
    /// X component, dimension `D`, in SI base units.
    pub x: Quantity<D, SI<f64>, f64>,
    /// Y component, dimension `D`, in SI base units.
    pub y: Quantity<D, SI<f64>, f64>,
    /// Z component, dimension `D`, in SI base units.
    pub z: Quantity<D, SI<f64>, f64>,
    _f: PhantomData<F>,
}

impl<D: ?Sized + Dimension, F: Frame> Qty3<D, F> {
    /// Construct from three scalar quantities of dimension `D`.
    #[inline]
    pub const fn new(
        x: Quantity<D, SI<f64>, f64>,
        y: Quantity<D, SI<f64>, f64>,
        z: Quantity<D, SI<f64>, f64>,
    ) -> Self {
        Self {
            x,
            y,
            z,
            _f: PhantomData,
        }
    }

    /// Raw SI-unit `DVec3` view.
    ///
    /// This is a value copy — but since each `Quantity<D, SI<f64>, f64>` is
    /// layout-equivalent to `f64` (all type parameters land in `PhantomData`
    /// fields which are zero-sized), the compiler lowers this to three field
    /// moves with no unit-conversion arithmetic. Inlined and monomorphic.
    #[inline(always)]
    pub fn raw_si(&self) -> DVec3 {
        DVec3::new(self.x.value, self.y.value, self.z.value)
    }

    /// Inverse of [`Self::raw_si`]: wrap a `DVec3` of SI-unit values as a typed
    /// [`Qty3`] in frame `F` and dimension `D`.
    ///
    /// The caller is responsible for providing values that are already in the
    /// dimension's base SI unit — this constructor performs no conversion.
    #[inline(always)]
    pub fn from_raw_si(v: DVec3) -> Self {
        Self {
            x: Quantity {
                dimension: PhantomData,
                units: PhantomData,
                value: v.x,
            },
            y: Quantity {
                dimension: PhantomData,
                units: PhantomData,
                value: v.y,
            },
            z: Quantity {
                dimension: PhantomData,
                units: PhantomData,
                value: v.z,
            },
            _f: PhantomData,
        }
    }

    /// Zero vector in frame `F`, dimension `D`.
    #[inline]
    pub fn zero() -> Self {
        Self::from_raw_si(DVec3::ZERO)
    }

    /// Relabel the frame phantom from `F` to `F2` without changing the
    /// underlying numeric values.
    ///
    /// This is a pure phantom-tag swap — the SI-unit components are
    /// preserved bit-identically. The method exists so call sites that
    /// need to assert "the same vector lives in a different conceptual
    /// frame" (e.g., RF.10 non-shift relabels, integ-origin shift
    /// preludes that already added the offset, atmosphere
    /// `IntegrationFrame` ↔ `PlanetInertial<P>` boundaries) can do so
    /// without round-tripping through raw `DVec3` and re-wrapping via
    /// `from_raw_si`. The relabel is **caller-asserted**, mirroring
    /// `from_raw_si` — there is no runtime check that `F` and `F2` are
    /// the same physical frame; the caller is responsible for the
    /// invariant.
    ///
    /// Use this in preference to `from_raw_si(self.raw_si())` whenever
    /// the source already carries a typed phantom and the destination
    /// is a different (caller-asserted equivalent) frame, so the
    /// escape-hatch lint can stay clean.
    #[inline(always)]
    pub fn relabel_to<F2: Frame>(self) -> Qty3<D, F2> {
        Qty3 {
            x: self.x,
            y: self.y,
            z: self.z,
            _f: PhantomData,
        }
    }
}

// Phantom-only generic: `#[derive]` would wrongly demand `D: Copy`,
// `F: Copy + Debug + PartialEq`, etc.
impl_copy_phantom!([D: ?Sized + Dimension, F: Frame] Qty3[D, F]);
impl_clone_phantom!([D: ?Sized + Dimension, F: Frame] Qty3[D, F]);
// `F::NAME` is the *kind* of the frame (e.g. "BodyFrame"). For vehicle- or
// planet-parameterized frames that alone can't distinguish `BodyFrame<Iss>`
// from `BodyFrame<Mir>`. Use `type_name::<F>()` so the output carries the
// full phantom tag — the cost is a single opaque string, no allocations.
impl_debug_phantom!(
    [D: ?Sized + Dimension, F: Frame] Qty3[D, F],
    |this, f| write!(
        f,
        "Qty3<{}>({}, {}, {})",
        core::any::type_name::<F>(),
        this.x.value,
        this.y.value,
        this.z.value
    )
);
impl_partial_eq_phantom!(
    [D: ?Sized + Dimension, F: Frame] Qty3[D, F],
    |this, other| this.x.value == other.x.value
        && this.y.value == other.y.value
        && this.z.value == other.z.value
);

/// `Default` returns the zero vector of dimension `D` in frame `F`. Manual
/// impl rather than derive, because `derive(Default)` would demand
/// `D: Default` (it isn't a real value, just a phantom dimension).
impl<D: ?Sized + Dimension, F: Frame> Default for Qty3<D, F> {
    #[inline]
    fn default() -> Self {
        Self::zero()
    }
}

// `serde` support is hand-written (not derived) for the same reason the other
// trait impls above are: the `D`/`F` type parameters are phantom and must not
// leak into the serialized form or generate spurious `D: Serialize` bounds. A
// `Qty3` round-trips as its raw-SI `[x, y, z]` triple — the dimension and frame
// are carried by the static type at the deserialization site, so they need no
// runtime encoding.
#[cfg(feature = "serde")]
impl<D: ?Sized + Dimension, F: Frame> serde::Serialize for Qty3<D, F> {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let v = self.raw_si();
        [v.x, v.y, v.z].serialize(serializer)
    }
}

#[cfg(feature = "serde")]
impl<'de, D: ?Sized + Dimension, F: Frame> serde::Deserialize<'de> for Qty3<D, F> {
    fn deserialize<De: serde::Deserializer<'de>>(deserializer: De) -> Result<Self, De::Error> {
        let [x, y, z] = <[f64; 3]>::deserialize(deserializer)?;
        Ok(Self::from_raw_si(DVec3::new(x, y, z)))
    }
}