amari_calculus/fields/
vector_field.rs

1//! Vector field implementation
2
3use crate::{CalculusError, CalculusResult};
4use amari_core::Multivector;
5
6/// A vector field F: ℝⁿ → Cl(p,q,r)_1 (grade-1 multivectors)
7///
8/// Represents a function that maps points in n-dimensional space to vectors (grade-1 multivectors).
9///
10/// # Examples
11///
12/// ```
13/// use amari_calculus::{VectorField, vector_from_slice};
14///
15/// // Define F(x, y, z) = (x, y, z) - radial vector field
16/// let f = VectorField::<3, 0, 0>::new(|coords| {
17///     vector_from_slice(&[coords[0], coords[1], coords[2]])
18/// });
19///
20/// // Evaluate at (1, 2, 3)
21/// let value = f.evaluate(&[1.0, 2.0, 3.0]);
22/// ```
23#[derive(Clone)]
24pub struct VectorField<const P: usize, const Q: usize, const R: usize> {
25    /// The function defining the field
26    function: fn(&[f64]) -> Multivector<P, Q, R>,
27    /// Domain dimension
28    dim: usize,
29}
30
31impl<const P: usize, const Q: usize, const R: usize> VectorField<P, Q, R> {
32    /// Create a new vector field from a function
33    ///
34    /// # Arguments
35    ///
36    /// * `function` - Function mapping coordinates to vectors
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use amari_calculus::{VectorField, vector_from_slice};
42    ///
43    /// // Rotation field F(x, y) = (-y, x, 0)
44    /// let f = VectorField::<3, 0, 0>::new(|coords| {
45    ///     vector_from_slice(&[-coords[1], coords[0], 0.0])
46    /// });
47    /// ```
48    pub fn new(function: fn(&[f64]) -> Multivector<P, Q, R>) -> Self {
49        Self {
50            function,
51            dim: P + Q + R,
52        }
53    }
54
55    /// Create a vector field with explicit dimension
56    pub fn with_dimension(function: fn(&[f64]) -> Multivector<P, Q, R>, dim: usize) -> Self {
57        Self { function, dim }
58    }
59
60    /// Evaluate the vector field at a point
61    ///
62    /// # Arguments
63    ///
64    /// * `coords` - Coordinates of the point
65    ///
66    /// # Returns
67    ///
68    /// The vector value at the point
69    pub fn evaluate(&self, coords: &[f64]) -> Multivector<P, Q, R> {
70        (self.function)(coords)
71    }
72
73    /// Get the domain dimension
74    pub fn dimension(&self) -> usize {
75        self.dim
76    }
77
78    /// Compute numerical derivative of vector field component along coordinate axis
79    ///
80    /// # Arguments
81    ///
82    /// * `coords` - Point at which to compute derivative
83    /// * `axis` - Coordinate axis index
84    /// * `h` - Step size (default: 1e-5)
85    pub fn partial_derivative(
86        &self,
87        coords: &[f64],
88        axis: usize,
89        h: f64,
90    ) -> CalculusResult<Multivector<P, Q, R>> {
91        if axis >= self.dim {
92            return Err(CalculusError::InvalidDimension {
93                expected: self.dim,
94                got: axis,
95            });
96        }
97
98        let mut coords_plus = coords.to_vec();
99        let mut coords_minus = coords.to_vec();
100
101        coords_plus[axis] += h;
102        coords_minus[axis] -= h;
103
104        let f_plus = self.evaluate(&coords_plus);
105        let f_minus = self.evaluate(&coords_minus);
106
107        // Compute (f_plus - f_minus) / (2h)
108        let result = (f_plus - f_minus) * (1.0 / (2.0 * h));
109
110        Ok(result)
111    }
112
113    // Note: Methods like add(), scale(), and dot() that combine fields
114    // are not implemented because function pointers cannot capture variables.
115    // Users should create new VectorField instances manually when combining fields.
116}
117
118impl<const P: usize, const Q: usize, const R: usize> std::fmt::Debug for VectorField<P, Q, R> {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("VectorField")
121            .field("dim", &self.dim)
122            .field("signature", &format!("Cl({},{},{})", P, Q, R))
123            .finish()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_vector_field_evaluation() {
133        // F(x, y, z) = (x, y, z) - radial field
134        let f = VectorField::<3, 0, 0>::new(|coords| {
135            crate::vector_from_slice(&[coords[0], coords[1], coords[2]])
136        });
137
138        let v = f.evaluate(&[1.0, 2.0, 3.0]);
139        let expected = crate::vector_from_slice::<3, 0, 0>(&[1.0, 2.0, 3.0]);
140
141        // Check components
142        for i in 0..3 {
143            assert!(
144                (v.vector_component(i) - expected.vector_component(i)).abs() < 1e-10,
145                "Component {} mismatch",
146                i
147            );
148        }
149    }
150
151    #[test]
152    fn test_vector_field_combination() {
153        // Test that we can manually combine vector fields
154        // F(x, y) = (x, y, 0)
155        let _f = VectorField::<3, 0, 0>::new(|coords| {
156            crate::vector_from_slice(&[coords[0], coords[1], 0.0])
157        });
158
159        // G(x, y) = (y, x, 0)
160        let _g = VectorField::<3, 0, 0>::new(|coords| {
161            crate::vector_from_slice(&[coords[1], coords[0], 0.0])
162        });
163
164        // H(x, y) = F + G = (x+y, y+x, 0)
165        let h = VectorField::<3, 0, 0>::new(|coords| {
166            crate::vector_from_slice(&[coords[0] + coords[1], coords[1] + coords[0], 0.0])
167        });
168
169        let v = h.evaluate(&[2.0, 3.0, 0.0]);
170        // Should be (5, 5, 0)
171        assert!((v.vector_component(0) - 5.0).abs() < 1e-10);
172        assert!((v.vector_component(1) - 5.0).abs() < 1e-10);
173    }
174
175    #[test]
176    fn test_partial_derivative() {
177        // F(x, y) = (x², y², 0)
178        // ∂F/∂x = (2x, 0, 0), ∂F/∂y = (0, 2y, 0)
179        let f = VectorField::<3, 0, 0>::new(|coords| {
180            crate::vector_from_slice(&[coords[0].powi(2), coords[1].powi(2), 0.0])
181        });
182
183        let df_dx = f.partial_derivative(&[3.0, 4.0, 0.0], 0, 1e-5).unwrap();
184        let df_dy = f.partial_derivative(&[3.0, 4.0, 0.0], 1, 1e-5).unwrap();
185
186        // ∂F/∂x at (3,4) should be approximately (6, 0, 0)
187        assert!(
188            (df_dx.vector_component(0) - 6.0).abs() < 1e-4,
189            "∂F_x/∂x should be 6.0"
190        );
191        assert!(
192            df_dx.vector_component(1).abs() < 1e-4,
193            "∂F_y/∂x should be 0.0"
194        );
195
196        // ∂F/∂y at (3,4) should be approximately (0, 8, 0)
197        assert!(
198            df_dy.vector_component(0).abs() < 1e-4,
199            "∂F_x/∂y should be 0.0"
200        );
201        assert!(
202            (df_dy.vector_component(1) - 8.0).abs() < 1e-4,
203            "∂F_y/∂y should be 8.0"
204        );
205    }
206}