deep_causality_physics/mhd/
ideal.rs

1/*
2 * SPDX-License-Identifier: MIT
3 * Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
4 */
5use crate::mhd::quantities::{AlfvenSpeed, MagneticPressure};
6use crate::{Density, PhysicalField, PhysicsError};
7use deep_causality_multivector::MultiVector;
8use deep_causality_sparse::CsrMatrix;
9use deep_causality_tensor::CausalTensor;
10use deep_causality_topology::{Manifold, SimplicialComplex};
11use std::collections::HashMap;
12
13/// Calculates the characteristic speed of Alfven waves.
14/// $$ v_A = \frac{B}{\sqrt{\mu_0 \rho}} $$
15///
16/// # Arguments
17/// *   `b_field` - Magnetic field $B$ (Uses magnitude $|B|$).
18/// *   `density` - Plasma density $\rho$.
19/// *   `permeability` - Magnetic permeability $\mu_0$.
20///
21/// # Returns
22/// *   `Result<AlfvenSpeed, PhysicsError>` - Alfven speed $v_A$.
23pub fn alfven_speed_kernel(
24    b_field: &PhysicalField,
25    density: &Density,
26    permeability: f64,
27) -> Result<AlfvenSpeed, PhysicsError> {
28    let b_mag = b_field.inner().squared_magnitude().sqrt();
29    let rho = density.value();
30
31    if permeability <= 0.0 {
32        return Err(PhysicsError::PhysicalInvariantBroken(
33            "Permeability must be positive".into(),
34        ));
35    }
36
37    if rho < 0.0 {
38        return Err(PhysicsError::PhysicalInvariantBroken(
39            "Density cannot be negative".into(),
40        ));
41    }
42
43    if rho == 0.0 {
44        return Err(PhysicsError::Singularity(
45            "Zero density in Alfven speed".into(),
46        ));
47    }
48
49    let denom = (permeability * rho).sqrt();
50    let va = b_mag / denom;
51
52    AlfvenSpeed::new(va)
53}
54
55/// Calculates magnetic pressure.
56/// $$ P_B = \frac{B^2}{2\mu_0} $$
57///
58/// # Arguments
59/// *   `b_field` - Magnetic field $B$.
60/// *   `permeability` - Magnetic permeability $\mu_0$.
61///
62/// # Returns
63/// *   `Result<MagneticPressure, PhysicsError>` - Magnetic pressure $P_B$.
64pub fn magnetic_pressure_kernel(
65    b_field: &PhysicalField,
66    permeability: f64,
67) -> Result<MagneticPressure, PhysicsError> {
68    let b_sq = b_field.inner().squared_magnitude();
69
70    if permeability <= 0.0 {
71        return Err(PhysicsError::PhysicalInvariantBroken(
72            "Permeability must be positive".into(),
73        ));
74    }
75
76    let pb = b_sq / (2.0 * permeability);
77    MagneticPressure::new(pb)
78}
79
80/// Calculates the time evolution of the magnetic field (Frozen-in flux).
81/// $$ \frac{\partial \mathbf{B}}{\partial t} = \nabla \times (\mathbf{v} \times \mathbf{B}) $$
82///
83/// **Geometric Algebra Implementation**:
84/// In the language of differential forms/GA on a Manifold:
85/// $$ \partial_t B = -d(i_v B) $$
86/// where $B$ is a 2-form (flux), $v$ is a vector field (represented as a 1-form),
87/// $i_v$ is interior product (contraction), and $d$ is exterior derivative.
88///
89/// This implementation relies on the identity:
90/// $$ i_v B = \star (v \wedge \star B) $$
91/// (valid for 3D manifolds where $v$ and $\star B$ are 1-forms).
92///
93/// # Arguments
94/// *   `v_manifold` - Manifold containing the velocity field $v$ (1-form).
95/// *   `b_manifold` - Manifold containing the magnetic flux 2-form $B$.
96///
97/// # Returns
98/// *   `Result<CausalTensor<f64>, PhysicsError>` - Rate of change of B (2-form), i.e., $-\partial_t B$.
99///     Wait, the equation is $\partial_t B = \dots$. The function returns $\partial_t B$.
100pub fn ideal_induction_kernel(
101    v_manifold: &Manifold<f64>,
102    b_manifold: &Manifold<f64>,
103) -> Result<CausalTensor<f64>, PhysicsError> {
104    // 1. Validation
105    let complex = v_manifold.complex();
106    let skeletons = complex.skeletons();
107
108    // Need at least 0, 1, 2 skeletons
109    if skeletons.len() < 3 {
110        return Err(PhysicsError::DimensionMismatch(
111            "Manifold must be at least 2D (preferably 3D) for induction".into(),
112        ));
113    }
114
115    let n0 = skeletons[0].simplices().len();
116    let n1 = skeletons[1].simplices().len();
117    let n2 = skeletons[2].simplices().len();
118
119    // Verify data lengths (Manifold enforces this on creation, but checks are cheap)
120    if v_manifold.data().len() < n0 + n1 + n2 {
121        return Err(PhysicsError::DimensionMismatch(
122            "v_manifold data too small".into(),
123        ));
124    }
125
126    // 2. Extract Data Slices
127    // v is 1-form: offset = n0, len = n1
128    let v_offset = n0;
129    let v_slice = &v_manifold.data().as_slice()[v_offset..v_offset + n1];
130
131    // B is 2-form: offset = n0 + n1, len = n2
132    let b_offset = n0 + n1;
133    let b_slice = &b_manifold.data().as_slice()[b_offset..b_offset + n2];
134
135    // 3. Compute Hodge Star of B (star_b)
136    // star_b: 2-form -> 1-form (in 3D)
137    // Using hodge_star_operators[2]
138    if complex.hodge_star_operators().len() <= 2 {
139        return Err(PhysicsError::CalculationError(
140            "Hodge star operator for 2-forms not available".into(),
141        ));
142    }
143    let h_star_2 = &complex.hodge_star_operators()[2];
144    let star_b_data = apply_csr_f64(h_star_2, b_slice);
145
146    // 4. Compute Wedge Product: v ^ star_b
147    // v: 1-form, star_b: 1-form -> Result: 2-form
148    let wedge_data = wedge_product_1form_1form(v_slice, &star_b_data, complex)?;
149
150    // 5. Compute Interior Product proxy: iv_b = star(v ^ star_b)
151    // wedge_data is 2-form. star maps to 1-form.
152    let iv_b_data = apply_csr_f64(h_star_2, &wedge_data);
153
154    // 6. Compute Exterior Derivative: d(iv_b)
155    // iv_b is 1-form. d maps to 2-form.
156    // Use coboundary_operators[1].
157    if complex.coboundary_operators().len() <= 1 {
158        return Err(PhysicsError::CalculationError(
159            "Coboundary operator for 1-forms not available".into(),
160        ));
161    }
162    let d_1 = &complex.coboundary_operators()[1];
163    let dt_b_neg_data = apply_csr_i8_f64(d_1, &iv_b_data);
164
165    // 7. Result
166    // Returns the 2-form part of the change.
167    let result_len = dt_b_neg_data.len();
168    CausalTensor::new(dt_b_neg_data, vec![result_len]).map_err(PhysicsError::from)
169}
170
171// --- Helper Functions ---
172
173/// Multiplies a CsrMatrix<f64> by a dense vector &[f64].
174fn apply_csr_f64(matrix: &CsrMatrix<f64>, vector: &[f64]) -> Vec<f64> {
175    let (rows, _cols) = matrix.shape();
176    let mut result = vec![0.0; rows];
177
178    // Using public getters
179    let row_indices = matrix.row_indices();
180    let col_indices = matrix.col_indices();
181    let values = matrix.values();
182
183    for i in 0..rows {
184        let start = row_indices[i];
185        let end = row_indices[i + 1];
186        let mut sum = 0.0;
187        for idx in start..end {
188            let col = col_indices[idx];
189            let val = values[idx];
190            if col < vector.len() {
191                sum += val * vector[col];
192            }
193        }
194        result[i] = sum;
195    }
196    result
197}
198
199/// Multiplies a CsrMatrix<i8> by a dense vector &[f64].
200fn apply_csr_i8_f64(matrix: &CsrMatrix<i8>, vector: &[f64]) -> Vec<f64> {
201    let (rows, _cols) = matrix.shape();
202    let mut result = vec![0.0; rows];
203
204    let row_indices = matrix.row_indices();
205    let col_indices = matrix.col_indices();
206    let values = matrix.values();
207
208    for i in 0..rows {
209        let start = row_indices[i];
210        let end = row_indices[i + 1];
211        let mut sum = 0.0;
212        for idx in start..end {
213            let col = col_indices[idx];
214            let val = values[idx]; // i8
215            if col < vector.len() {
216                sum += (val as f64) * vector[col];
217            }
218        }
219        result[i] = sum;
220    }
221    result
222}
223
224/// Computes the Wedge Product of two 1-forms on a Simplicial Complex.
225/// Result is a 2-form.
226///
227/// Formula used (Cup Product):
228/// $(\alpha \cup \beta)([0,1,2]) = \alpha([0,1]) \cdot \beta([1,2])$
229/// Wedge Product $\alpha \wedge \beta = \alpha \cup \beta - \beta \cup \alpha$.
230fn wedge_product_1form_1form(
231    alpha: &[f64],
232    beta: &[f64],
233    complex: &SimplicialComplex,
234) -> Result<Vec<f64>, PhysicsError> {
235    let skeletons = complex.skeletons();
236    if skeletons.len() < 3 {
237        return Err(PhysicsError::DimensionMismatch(
238            "Complex must have 2-simplices".into(),
239        ));
240    }
241    let edges = skeletons[1].simplices();
242    let faces = skeletons[2].simplices();
243
244    // Build Edge Lookup Map: (min(u,v), max(u,v)) -> edge_index
245    // This is O(E)
246    let mut edge_map = HashMap::with_capacity(edges.len());
247    for (idx, edge_simplex) in edges.iter().enumerate() {
248        let verts = edge_simplex.vertices();
249        if verts.len() >= 2 {
250            edge_map.insert((verts[0], verts[1]), idx);
251        }
252    }
253
254    let mut result = Vec::with_capacity(faces.len());
255
256    // Iterate over faces (2-simplices)
257    for face in faces {
258        let verts = face.vertices(); // Sorted [v0, v1, v2]
259        if verts.len() != 3 {
260            // Non-triangular face? Skip or zero.
261            result.push(0.0);
262            continue;
263        }
264        let v0 = verts[0];
265        let v1 = verts[1];
266        let v2 = verts[2];
267
268        // Edges for Cup Product
269        // [v0, v1] and [v1, v2]
270        let e01_idx = edge_map.get(&(v0, v1));
271        let e12_idx = edge_map.get(&(v1, v2));
272
273        // Check if edges exist
274        if let (Some(&idx01), Some(&idx12)) = (e01_idx, e12_idx) {
275            let val_alpha_01 = alpha.get(idx01).unwrap_or(&0.0);
276            let val_beta_12 = beta.get(idx12).unwrap_or(&0.0);
277            let val_beta_01 = beta.get(idx01).unwrap_or(&0.0);
278            let val_alpha_12 = alpha.get(idx12).unwrap_or(&0.0);
279
280            // \alpha \wedge \beta = \alpha \cup \beta - \beta \cup \alpha
281            let term1 = val_alpha_01 * val_beta_12;
282            let term2 = val_beta_01 * val_alpha_12;
283
284            result.push(term1 - term2);
285        } else {
286            result.push(0.0);
287        }
288    }
289
290    Ok(result)
291}