geoit 0.0.2

Exact geometric algebra with governed multivectors
Documentation
//! Field interpretation: how a geometric class's Mv is evaluated at a probe point.
//!
//! Each `GeomClass` declares a `FieldOp` that determines the algebraic operation
//! used to evaluate the implicit field. The evaluator returns a full `Mv` — the
//! caller decides how to interpret it (scalar part, vanishing test, norm, etc.).
//!
//! Common interpretations:
//! - IPNS (Inner Product Null Space): `⟨probe, X⟩` — CGA points, spheres
//! - OPNS (Outer Product Null Space): `probe ∧ X` — objects defined as wedge of points
//! - Left contraction: `probe ⌋ X` — PGA lines, planes
//! - Custom: arbitrary expression using `Probe` and `Object` Expr nodes

use crate::algebra::mv::Mv;
use crate::algebra::ops;
use crate::algebra::signature::Signature;
use crate::scalar::Scalar;

/// How a geometric class's Mv is evaluated against a probe point.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub enum FieldOp {
    /// ⟨probe, X⟩ — scalar product (IPNS). Default for CGA.
    #[default]
    ScalarProduct,
    /// probe ∧ X — outer product (OPNS). Zero iff probe lies on the object.
    OuterProduct,
    /// probe ⌋ X — left contraction. Natural for PGA objects.
    LeftContraction,
    /// ⟨probe * X⟩_k — grade-k component of geometric product.
    GradeProduct(u8),
    /// probe · X (Hestenes inner product).
    InnerProduct,
    /// probe * X (full geometric product).
    GeometricProduct,
}

/// Which construction to use for probe points, and spatial arity.
#[derive(Clone, Debug)]
pub struct ProbeSpec {
    /// Index into governance.constructions for the probe builder.
    pub construction_index: usize,
    /// Number of spatial parameters (2 for 2D rendering, 3 for 3D).
    pub arity: usize,
}

impl FieldOp {
    /// Evaluate the field operation, returning the full result Mv.
    ///
    /// The caller decides interpretation:
    /// - For IPNS: the scalar part is the field value
    /// - For OPNS: zero Mv means probe lies on the object
    /// - For PGA left contraction: the result Mv IS the projected object
    pub fn evaluate_mv(&self, probe: &Mv, object: &Mv, sig: &Signature) -> Mv {
        match self {
            FieldOp::ScalarProduct => ops::scalar_product(probe, object, sig),
            FieldOp::OuterProduct => ops::outer(probe, object, sig),
            FieldOp::LeftContraction => ops::left_contract(probe, object, sig),
            FieldOp::GradeProduct(k) => {
                let result = ops::geometric(probe, object, sig);
                ops::grade_project(&result, *k)
            }
            FieldOp::InnerProduct => ops::inner(probe, object, sig),
            FieldOp::GeometricProduct => ops::geometric(probe, object, sig),
        }
    }

    /// Convenience: evaluate and extract the scalar part.
    /// For ScalarProduct this is exact; for other ops it's the grade-0 component.
    pub fn evaluate_scalar(&self, probe: &Mv, object: &Mv, sig: &Signature) -> Scalar {
        let result = self.evaluate_mv(probe, object, sig);
        result.coefficient(0)
    }

    /// Convenience: evaluate and return the squared norm of the result.
    /// Useful for rendering: sign determines inside/outside, magnitude gives distance.
    pub fn evaluate_norm_sq(&self, probe: &Mv, object: &Mv, sig: &Signature) -> Scalar {
        let result = self.evaluate_mv(probe, object, sig);
        ops::norm_squared(&result, sig)
    }

    /// Convenience: test if the field result vanishes (probe on object surface).
    pub fn is_on_surface(&self, probe: &Mv, object: &Mv, sig: &Signature) -> bool {
        self.evaluate_mv(probe, object, sig).is_zero()
    }

    /// Parse a field op keyword from a string.
    pub fn from_keyword(s: &str) -> Option<Self> {
        match s {
            "ipns" | "scalar_product" => Some(FieldOp::ScalarProduct),
            "opns" | "outer_product" => Some(FieldOp::OuterProduct),
            "lcontract" | "left_contraction" => Some(FieldOp::LeftContraction),
            "inner" | "inner_product" => Some(FieldOp::InnerProduct),
            "geometric" | "geometric_product" => Some(FieldOp::GeometricProduct),
            _ => None,
        }
    }

    /// Keyword string for emission.
    pub fn keyword(&self) -> String {
        match self {
            FieldOp::ScalarProduct => "ipns".to_string(),
            FieldOp::OuterProduct => "opns".to_string(),
            FieldOp::LeftContraction => "lcontract".to_string(),
            FieldOp::GradeProduct(k) => format!("grade_product({})", k),
            FieldOp::InnerProduct => "inner".to_string(),
            FieldOp::GeometricProduct => "geometric".to_string(),
        }
    }
}

impl std::fmt::Display for FieldOp {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.keyword())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::scalar::Rat;

    #[test]
    fn default_is_scalar_product() {
        assert!(matches!(FieldOp::default(), FieldOp::ScalarProduct));
    }

    #[test]
    fn keyword_roundtrip() {
        for op in &[
            FieldOp::ScalarProduct,
            FieldOp::OuterProduct,
            FieldOp::LeftContraction,
            FieldOp::InnerProduct,
            FieldOp::GeometricProduct,
        ] {
            let kw = op.keyword();
            let parsed = FieldOp::from_keyword(&kw).unwrap();
            assert_eq!(parsed.keyword(), kw);
        }
    }

    #[test]
    fn scalar_product_returns_scalar_mv() {
        let sig = Signature::new(0, 0, 3).unwrap();
        let probe = Mv::from_rat_terms(&[(0b001, Rat::from(1))]);
        let object = Mv::from_rat_terms(&[(0b001, Rat::from(1))]);
        let result = FieldOp::ScalarProduct.evaluate_mv(&probe, &object, &sig);
        assert_eq!(result.coefficient(0), Scalar::from(1i64));
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn scalar_product_orthogonal() {
        let sig = Signature::new(0, 0, 3).unwrap();
        let probe = Mv::from_rat_terms(&[(0b001, Rat::from(1))]);
        let object = Mv::from_rat_terms(&[(0b010, Rat::from(1))]);
        let val = FieldOp::ScalarProduct.evaluate_scalar(&probe, &object, &sig);
        assert_eq!(val, Scalar::from(0i64));
    }

    #[test]
    fn outer_product_returns_full_mv() {
        let sig = Signature::new(0, 0, 3).unwrap();
        let a = Mv::from_rat_terms(&[(0b001, Rat::from(1))]); // e0
        let b = Mv::from_rat_terms(&[(0b010, Rat::from(1))]); // e1
        let result = FieldOp::OuterProduct.evaluate_mv(&a, &b, &sig);
        // e0 ∧ e1 = e01 (grade 2 bivector)
        assert_eq!(result.coefficient(0b011), Scalar::from(1i64));
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn left_contraction_returns_full_mv() {
        // PGA2: e1 ⌋ (e1∧e2) should give e2
        let sig = Signature::new(0, 1, 2).unwrap();
        let probe = Mv::from_rat_terms(&[(0b010, Rat::from(1))]); // e1 (h0)
        let object = Mv::from_rat_terms(&[(0b110, Rat::from(1))]); // e1∧e2 (h0∧h1)
        let result = FieldOp::LeftContraction.evaluate_mv(&probe, &object, &sig);
        // Result should be proportional to e2
        assert!(!result.is_zero());
    }

    #[test]
    fn is_on_surface_test() {
        let sig = Signature::new(0, 0, 3).unwrap();
        let a = Mv::from_rat_terms(&[(0b001, Rat::from(1))]);
        // e0 ∧ e0 = 0 (self-wedge vanishes)
        assert!(FieldOp::OuterProduct.is_on_surface(&a, &a, &sig));
    }

    #[test]
    fn inner_product_field_op() {
        let sig = Signature::new(0, 0, 3).unwrap();
        let a = Mv::from_rat_terms(&[(0b001, Rat::from(3))]);
        let b = Mv::from_rat_terms(&[(0b001, Rat::from(4))]);
        let result = FieldOp::InnerProduct.evaluate_mv(&a, &b, &sig);
        // 3e0 · 4e0 = 12 (scalar)
        assert_eq!(result.coefficient(0), Scalar::from(12i64));
    }

    #[test]
    fn geometric_product_field_op() {
        let sig = Signature::new(0, 0, 3).unwrap();
        let a = Mv::from_rat_terms(&[(0b001, Rat::from(1))]);
        let b = Mv::from_rat_terms(&[(0b010, Rat::from(1))]);
        let result = FieldOp::GeometricProduct.evaluate_mv(&a, &b, &sig);
        // e0 * e1 = e01
        assert_eq!(result.coefficient(0b011), Scalar::from(1i64));
    }

    #[test]
    fn norm_sq_evaluation() {
        let sig = Signature::new(0, 0, 3).unwrap();
        let probe = Mv::from_rat_terms(&[(0b001, Rat::from(3))]);
        let object = Mv::from_rat_terms(&[(0b001, Rat::from(4))]);
        let ns = FieldOp::ScalarProduct.evaluate_norm_sq(&probe, &object, &sig);
        // scalar_product(3e0, 4e0) = 12, norm²(12) = 144
        assert_eq!(ns, Scalar::from(144i64));
    }
}