oxiproj-core 0.1.2

Foundation types for OxiProj: coordinates, errors, ellipsoids, datums, and units.
Documentation
//! Generic projection trait for automatic differentiation support.
//!
//! [`ProjectGeneric`] is the AD-aware counterpart to the object-safe
//! [`crate::operation::Operation`] trait. Unlike `Operation`, which is
//! intentionally kept object-safe and works on `f64` only, `ProjectGeneric`
//! is generic over any [`Scalar`], enabling projection
//! structs to be driven with dual numbers for exact Jacobian computation.
//!
//! # Design notes
//!
//! - **Not object-safe**: the generic method parameter makes this trait
//!   incompatible with `dyn Trait`. This is intentional: it is an additive
//!   path, not a replacement for `Operation`.
//! - **Zero run-time cost at `f64`**: when instantiated with `S = f64`, the
//!   compiler monomorphises to exactly the same code as a plain `f64` impl.
//! - **Parity preserved**: existing `factors.rs` finite-difference code is
//!   untouched. `factors_exact()` (Batch 2) will call `project_fwd_generic`
//!   with `Dual1<2>` to compute the exact Jacobian.

use crate::{autodiff::Dual1, error::ProjResult, scalar::Scalar};

/// Non-object-safe projection trait parameterised over a [`Scalar`] type.
///
/// Projection structs implement this trait (alongside [`crate::operation::Operation`])
/// to opt into the AD path. Implementing this trait for a struct makes it
/// possible to compute exact distortion factors by substituting `Dual1<2>`
/// for the scalar type.
///
/// # Type parameters
///
/// The implementor is generic over the *point* type rather than over `self`,
/// to keep `&self` immutable and avoid lifetime issues.
pub trait ProjectGeneric {
    /// Forward projection: `(lam, phi)` in normalised space → `(x, y)`.
    ///
    /// The normalised space uses a unit sphere (a = 1). Scaling by the
    /// semi-major axis is applied by the engine layer.
    fn project_fwd_generic<S: Scalar>(&self, lam: S, phi: S) -> ProjResult<(S, S)>;

    /// Inverse projection: `(x, y)` in normalised space → `(lam, phi)`.
    fn project_inv_generic<S: Scalar>(&self, x: S, y: S) -> ProjResult<(S, S)>;

    /// Whether this projection's generic forward is **SIMD-lane-safe**.
    ///
    /// Returns `true` only if [`project_fwd_generic`](Self::project_fwd_generic)
    /// contains **no data-dependent branches**: no `if`/`match` — and no domain,
    /// quadrant, or hemisphere test — whose outcome depends on the *value* of
    /// `lam` or `phi`. When that holds, all four lanes of a 4-wide SIMD batch
    /// provably take the same code path, so a SIMD forward yields the same
    /// per-lane result the scalar path would.
    ///
    /// The default is `false`, the conservative and always-correct answer. A
    /// forward that branches on a lane's value (for example
    /// `if phi.to_f64() < 0.0 { .. }`) MUST leave this at `false`: on a 4-wide
    /// SIMD batch the branch would be decided by lane 0 alone, so a chunk
    /// straddling that boundary would drive lanes 1..3 down lane 0's branch and
    /// silently corrupt them. Overriding to `true` is a pure *performance*
    /// opt-in, sound only after auditing the forward for branch-freedom; the
    /// batch driver stays bit-for-bit correct either way.
    fn simd_lane_safe(&self) -> bool {
        false
    }
}

/// Object-safe wrapper: monomorphises [`ProjectGeneric`] for `Dual1<2>`.
///
/// Because `ProjectGeneric` is not object-safe (it has a generic method),
/// this trait provides a concrete, object-safe interface for the 2-D Jacobian
/// computation. A blanket implementation is provided for any `T` that
/// implements [`ProjectGeneric`].
///
/// Stored in `Pj` as `Arc<dyn ProjectGenericBox>` to avoid repeated monomorphisation.
pub trait ProjectGenericBox: Send + Sync + core::fmt::Debug {
    /// Evaluate the forward projection with `Dual1<2>` inputs to obtain the
    /// full 2×2 Jacobian.
    fn project_fwd_dual2(&self, lam: Dual1<2>, phi: Dual1<2>) -> ProjResult<(Dual1<2>, Dual1<2>)>;

    /// Evaluate the inverse projection with `Dual1<2>` inputs.
    fn project_inv_dual2(&self, x: Dual1<2>, y: Dual1<2>) -> ProjResult<(Dual1<2>, Dual1<2>)>;
}

/// Blanket implementation: any `T: ProjectGeneric + Send + Sync + Debug`
/// automatically satisfies `ProjectGenericBox`.
impl<T> ProjectGenericBox for T
where
    T: ProjectGeneric + Send + Sync + core::fmt::Debug,
{
    fn project_fwd_dual2(&self, lam: Dual1<2>, phi: Dual1<2>) -> ProjResult<(Dual1<2>, Dual1<2>)> {
        self.project_fwd_generic(lam, phi)
    }

    fn project_inv_dual2(&self, x: Dual1<2>, y: Dual1<2>) -> ProjResult<(Dual1<2>, Dual1<2>)> {
        self.project_inv_generic(x, y)
    }
}