Skip to main content

clifford_codegen/discovery/
mod.rs

1//! Entity discovery for geometric algebra code generation.
2//!
3//! This module automatically discovers valid geometric entities from an algebra
4//! signature by analyzing geometric constraints.
5//!
6//! # Overview
7//!
8//! Given an algebra signature (p, q, r), entity discovery:
9//! 1. Enumerates all possible grade combinations
10//! 2. Analyzes which grade pairs conflict (create non-canceling terms)
11//! 3. Determines what additional constraints are needed for satisfaction
12//! 4. Names known entities using heuristics
13//! 5. Can generate a TOML template for further customization
14//!
15//! # Constraint Analysis
16//!
17//! Not all grade combinations automatically satisfy the geometric constraint
18//! `u * ũ = scalar`. For combinations that don't, we analyze:
19//! - Which pairs of grades create conflicting (non-canceling) terms
20//! - What constraints would be needed to satisfy the constraint
21//!
22//! For example, [0, 1] (scalar + vector) has a conflict because:
23//! - scalar * vector = vector, and these don't cancel
24//! - Constraint needed: either scalar = 0 OR vector = 0
25//!
26//! # Example
27//!
28//! ```
29//! use clifford_codegen::discovery::{discover_entities, analyze_constraints, DiscoveredEntity};
30//! use clifford_codegen::algebra::Algebra;
31//!
32//! let algebra = Algebra::euclidean(3);
33//!
34//! // Analyze what constraints would be needed for the full multivector
35//! let conflicts = analyze_constraints(&[0, 1, 2, 3], &algebra);
36//! // Returns: conflicting grade pairs like (0, 1), (0, 3), (1, 2), (2, 3)
37//!
38//! // Discover entities that automatically satisfy constraints
39//! let entities = discover_entities(&algebra);
40//! assert!(entities.iter().any(|e| e.name == "Entity_0_2")); // [0, 2] - no conflicts
41//! ```
42
43mod constraints;
44mod entity;
45mod naming;
46pub mod products;
47mod template;
48
49pub use constraints::{
50    can_satisfy_constraints, derive_antiproduct_constraint, derive_blade_constraint,
51    derive_field_constraint, derive_null_constraint,
52};
53pub use entity::DiscoveredEntity;
54pub use naming::suggest_name;
55pub use products::{
56    BladeProductResult, EntityBladeSet, ProductResult, ProductTable2D, ProductType,
57    infer_all_products, infer_all_products_blades, infer_output_blades, infer_output_grades,
58    infer_product, infer_product_blades,
59};
60pub use template::generate_toml_template;
61
62use crate::algebra::{
63    Algebra, ProductTable, blades_of_grades, grade, reverse_sign, satisfies_all_constraints,
64};
65
66/// Represents a conflict between two grades in the geometric constraint.
67///
68/// When grades `grade_a` and `grade_b` have a conflict, it means that
69/// elements with non-zero components in both grades will produce
70/// non-canceling cross-terms in `u * ũ`.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct GradeConflict {
73    /// First grade in the conflict.
74    pub grade_a: usize,
75    /// Second grade in the conflict.
76    pub grade_b: usize,
77    /// Description of why these grades conflict.
78    pub reason: String,
79}
80
81/// Analyzes which grade pairs conflict in the geometric constraint.
82///
83/// For a grade combination, this identifies pairs of grades where the
84/// cross-terms in `u * ũ` don't cancel, producing non-scalar results.
85///
86/// # Returns
87///
88/// A list of conflicting grade pairs. If empty, the grade combination
89/// automatically satisfies the geometric constraint.
90///
91/// # Example
92///
93/// ```
94/// use clifford_codegen::discovery::analyze_constraints;
95/// use clifford_codegen::algebra::Algebra;
96///
97/// let algebra = Algebra::euclidean(3);
98///
99/// // Rotor [0, 2] has no conflicts - cross-terms cancel
100/// let conflicts = analyze_constraints(&[0, 2], &algebra);
101/// assert!(conflicts.is_empty());
102///
103/// // Full multivector [0, 1, 2, 3] has conflicts
104/// let conflicts = analyze_constraints(&[0, 1, 2, 3], &algebra);
105/// assert!(!conflicts.is_empty());
106/// ```
107pub fn analyze_constraints(grades: &[usize], algebra: &Algebra) -> Vec<GradeConflict> {
108    let table = ProductTable::new(algebra);
109    let mut conflicts = Vec::new();
110
111    // Check each pair of grades
112    for (i, &ga) in grades.iter().enumerate() {
113        for &gb in &grades[i..] {
114            if ga == gb {
115                // Same grade - check if self-products are scalar
116                if !same_grade_satisfies(ga, algebra, &table) {
117                    conflicts.push(GradeConflict {
118                        grade_a: ga,
119                        grade_b: gb,
120                        reason: format!("Grade {} blades produce non-scalar self-products", ga),
121                    });
122                }
123            } else {
124                // Different grades - check if cross-terms cancel
125                if !cross_grades_cancel(ga, gb, algebra, &table) {
126                    conflicts.push(GradeConflict {
127                        grade_a: ga,
128                        grade_b: gb,
129                        reason: format!(
130                            "Grades {} and {} produce non-canceling cross-terms",
131                            ga, gb
132                        ),
133                    });
134                }
135            }
136        }
137    }
138
139    conflicts
140}
141
142/// Checks if blades of the same grade produce only scalar self-products.
143fn same_grade_satisfies(g: usize, algebra: &Algebra, table: &ProductTable) -> bool {
144    let blades = blades_of_grades(algebra.dim(), &[g]);
145    let rev_g = reverse_sign(g);
146
147    for (i, &a) in blades.iter().enumerate() {
148        // Check a * reverse(a)
149        let (sign_aa, result_aa) = table.geometric(a, a);
150        if sign_aa != 0 && grade(result_aa) != 0 {
151            return false;
152        }
153
154        // Check cross-products between same-grade blades
155        for &b in &blades[i + 1..] {
156            let (sign_ab, result_ab) = table.geometric(a, b);
157            let (sign_ba, _) = table.geometric(b, a);
158
159            if grade(result_ab) != 0 {
160                let total =
161                    i32::from(sign_ab) * i32::from(rev_g) + i32::from(sign_ba) * i32::from(rev_g);
162                if total != 0 {
163                    return false;
164                }
165            }
166        }
167    }
168
169    true
170}
171
172/// Checks if cross-terms between two different grades cancel.
173fn cross_grades_cancel(ga: usize, gb: usize, algebra: &Algebra, table: &ProductTable) -> bool {
174    let blades_a = blades_of_grades(algebra.dim(), &[ga]);
175    let blades_b = blades_of_grades(algebra.dim(), &[gb]);
176    let rev_a = reverse_sign(ga);
177    let rev_b = reverse_sign(gb);
178
179    for &a in &blades_a {
180        for &b in &blades_b {
181            let (sign_ab, result_ab) = table.geometric(a, b);
182            let (sign_ba, _) = table.geometric(b, a);
183
184            // If result is non-scalar, check if contributions cancel
185            if grade(result_ab) != 0 {
186                let total =
187                    i32::from(sign_ab) * i32::from(rev_b) + i32::from(sign_ba) * i32::from(rev_a);
188                if total != 0 {
189                    return false;
190                }
191            }
192        }
193    }
194
195    true
196}
197
198/// Suggests constraints needed to satisfy the geometric constraint.
199///
200/// Given a list of grade conflicts, suggests what constraints would
201/// make the grade combination satisfy `u * ũ = scalar`.
202///
203/// # Example
204///
205/// ```
206/// use clifford_codegen::discovery::{analyze_constraints, suggest_required_constraints};
207/// use clifford_codegen::algebra::Algebra;
208///
209/// let algebra = Algebra::euclidean(3);
210/// let conflicts = analyze_constraints(&[0, 1, 2, 3], &algebra);
211/// let suggestions = suggest_required_constraints(&conflicts);
212///
213/// // Will suggest something like: "At most one of {0,2} or {1,3} can be non-zero"
214/// ```
215pub fn suggest_required_constraints(conflicts: &[GradeConflict]) -> Vec<String> {
216    if conflicts.is_empty() {
217        return vec!["No additional constraints needed".to_string()];
218    }
219
220    let mut suggestions = Vec::new();
221
222    // Group conflicts by grade
223    let mut conflicting_grades: std::collections::HashSet<usize> = std::collections::HashSet::new();
224    for conflict in conflicts {
225        conflicting_grades.insert(conflict.grade_a);
226        conflicting_grades.insert(conflict.grade_b);
227    }
228
229    // Find maximal non-conflicting subsets
230    // This is a simplified heuristic - could be made more sophisticated
231    if conflicting_grades.len() > 2 {
232        suggestions.push(format!(
233            "Grades {:?} have mutual conflicts. Consider using subsets that satisfy constraints.",
234            conflicting_grades.iter().collect::<Vec<_>>()
235        ));
236    } else {
237        for conflict in conflicts {
238            suggestions.push(format!(
239                "Either grade {} or grade {} must be zero (constraint: {})",
240                conflict.grade_a, conflict.grade_b, conflict.reason
241            ));
242        }
243    }
244
245    suggestions
246}
247
248/// Enumerates all non-empty grade combinations for an n-dimensional algebra.
249///
250/// For an n-dimensional algebra with grades 0 through n, this generates
251/// all 2^(n+1) - 1 non-empty subsets of grades.
252///
253/// # Example
254///
255/// ```
256/// use clifford_codegen::discovery::enumerate_grade_combinations;
257///
258/// // 2D algebra has grades 0, 1, 2
259/// let combinations = enumerate_grade_combinations(2);
260/// assert_eq!(combinations.len(), 7); // 2^3 - 1 = 7 non-empty subsets
261/// ```
262pub fn enumerate_grade_combinations(dim: usize) -> Vec<Vec<usize>> {
263    let num_grades = dim + 1; // grades 0 through dim
264
265    // 2^(num_grades) - 1 non-empty subsets
266    (1..(1 << num_grades))
267        .map(|mask| (0..num_grades).filter(|&g| (mask >> g) & 1 == 1).collect())
268        .collect()
269}
270
271/// Discovers all valid grade combinations that satisfy geometric constraints.
272///
273/// Returns grade combinations that satisfy both:
274/// 1. Geometric product constraint: `u * ũ` produces only scalar
275/// 2. Antiproduct constraint: `u ⊟ ũ̃` produces only antiscalar
276///
277/// # Example
278///
279/// ```
280/// use clifford_codegen::discovery::discover_valid_combinations;
281/// use clifford_codegen::algebra::Algebra;
282///
283/// let algebra = Algebra::euclidean(3);
284/// let valid = discover_valid_combinations(&algebra);
285///
286/// // All single grades satisfy constraints
287/// assert!(valid.contains(&vec![0])); // Scalar
288/// assert!(valid.contains(&vec![1])); // Vector
289/// assert!(valid.contains(&vec![2])); // Bivector
290/// assert!(valid.contains(&vec![3])); // Trivector
291///
292/// // Even and odd subalgebras satisfy
293/// assert!(valid.contains(&vec![0, 2])); // Rotor
294/// assert!(valid.contains(&vec![1, 3])); // Odd
295/// ```
296pub fn discover_valid_combinations(algebra: &Algebra) -> Vec<Vec<usize>> {
297    let table = ProductTable::new(algebra);
298
299    enumerate_grade_combinations(algebra.dim())
300        .into_iter()
301        .filter(|grades| satisfies_all_constraints(grades, algebra, &table))
302        .collect()
303}
304
305/// Discovers the minimal closed set of geometric entities.
306///
307/// This returns the standard set of types that form a closed algebra:
308/// - All single-grade types (scalar, vector, bivector, etc.)
309/// - Even subalgebra (grades 0, 2, 4, ...) - motors/rotors
310/// - Odd subalgebra (grades 1, 3, 5, ...) - flectors
311///
312/// This minimal set is sufficient for most geometric algebra applications
313/// and matches the standard types used in PGA, CGA, etc.
314///
315/// For grade combinations that need field constraints (like PGA bivectors),
316/// the constraint expression is included in the entity.
317///
318/// # Example
319///
320/// ```
321/// use clifford_codegen::discovery::discover_entities;
322/// use clifford_codegen::algebra::Algebra;
323///
324/// let algebra = Algebra::pga(3);
325/// let entities = discover_entities(&algebra);
326///
327/// // Returns 7 entities for PGA:
328/// // [0], [1], [2], [3], [4], [0,2,4], [1,3]
329/// assert_eq!(entities.len(), 7);
330/// ```
331pub fn discover_entities(algebra: &Algebra) -> Vec<DiscoveredEntity> {
332    let dim = algebra.dim();
333
334    // Build the minimal set of grade combinations
335    let mut grade_sets: Vec<Vec<usize>> = Vec::new();
336
337    // Single grades (scalar, vector, bivector, ..., pseudoscalar)
338    for g in 0..=dim {
339        grade_sets.push(vec![g]);
340    }
341
342    // Even subalgebra (grades 0, 2, 4, ...) - only if more than one grade
343    let even: Vec<usize> = (0..=dim).step_by(2).collect();
344    if even.len() > 1 {
345        grade_sets.push(even);
346    }
347
348    // Odd subalgebra (grades 1, 3, 5, ...) - only if more than one grade
349    let odd: Vec<usize> = (1..=dim).step_by(2).collect();
350    if odd.len() > 1 {
351        grade_sets.push(odd);
352    }
353
354    // Convert to entities with constraints
355    grade_sets
356        .into_iter()
357        .map(|grades| {
358            let geometric_constraint = derive_field_constraint(&grades, algebra);
359            let antiproduct_constraint = derive_antiproduct_constraint(&grades, algebra);
360            let name = suggest_name(&grades, algebra.dim());
361
362            DiscoveredEntity {
363                name,
364                grades,
365                geometric_constraint,
366                antiproduct_constraint,
367            }
368        })
369        .collect()
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn enumerate_2d() {
378        let combinations = enumerate_grade_combinations(2);
379        // 2^3 - 1 = 7 non-empty subsets
380        assert_eq!(combinations.len(), 7);
381
382        // Check all single grades present
383        assert!(combinations.contains(&vec![0]));
384        assert!(combinations.contains(&vec![1]));
385        assert!(combinations.contains(&vec![2]));
386    }
387
388    #[test]
389    fn enumerate_3d() {
390        let combinations = enumerate_grade_combinations(3);
391        // 2^4 - 1 = 15 non-empty subsets
392        assert_eq!(combinations.len(), 15);
393    }
394
395    #[test]
396    fn discover_euclidean_3d() {
397        let algebra = Algebra::euclidean(3);
398        let valid = discover_valid_combinations(&algebra);
399
400        // Single grades
401        assert!(valid.contains(&vec![0])); // Scalar
402        assert!(valid.contains(&vec![1])); // Vector
403        assert!(valid.contains(&vec![2])); // Bivector
404        assert!(valid.contains(&vec![3])); // Trivector
405
406        // Even and odd subalgebras
407        assert!(valid.contains(&vec![0, 2])); // Rotor
408        assert!(valid.contains(&vec![1, 3])); // Odd
409
410        // Full multivector does NOT satisfy constraints
411        assert!(!valid.contains(&vec![0, 1, 2, 3]));
412    }
413
414    #[test]
415    fn discover_euclidean_2d() {
416        let algebra = Algebra::euclidean(2);
417        let valid = discover_valid_combinations(&algebra);
418
419        // Single grades
420        assert!(valid.contains(&vec![0])); // Scalar
421        assert!(valid.contains(&vec![1])); // Vector
422        assert!(valid.contains(&vec![2])); // Bivector
423
424        // Even subalgebra
425        assert!(valid.contains(&vec![0, 2])); // Rotor
426    }
427
428    #[test]
429    fn discover_entities_minimal_set() {
430        // Euclidean 3D: 4 single grades + even [0,2] + odd [1,3] = 6 entities
431        let algebra = Algebra::euclidean(3);
432        let entities = discover_entities(&algebra);
433        assert_eq!(entities.len(), 6);
434
435        // Check all expected entities are present
436        assert!(entities.iter().any(|e| e.grades == vec![0]));
437        assert!(entities.iter().any(|e| e.grades == vec![1]));
438        assert!(entities.iter().any(|e| e.grades == vec![2]));
439        assert!(entities.iter().any(|e| e.grades == vec![3]));
440        assert!(entities.iter().any(|e| e.grades == vec![0, 2]));
441        assert!(entities.iter().any(|e| e.grades == vec![1, 3]));
442    }
443
444    #[test]
445    fn discover_pga_entities_minimal_set() {
446        // PGA 3D (4D algebra): 5 single grades + even [0,2,4] + odd [1,3] = 7 entities
447        let algebra = Algebra::pga(3);
448        let entities = discover_entities(&algebra);
449        assert_eq!(entities.len(), 7);
450
451        // Check bivector has constraint(s)
452        let bivector = entities.iter().find(|e| e.grades == vec![2]).unwrap();
453        assert!(
454            bivector.has_constraints(),
455            "Bivector should have constraints"
456        );
457
458        // Check motor has constraint(s) - should have 2 constraints for 6 DOF
459        let motor = entities.iter().find(|e| e.grades == vec![0, 2, 4]).unwrap();
460        assert!(motor.has_constraints(), "Motor should have constraints");
461        // Motor should have 2 constraints (8 coeffs - 2 constraints = 6 DOF)
462        assert_eq!(
463            motor.constraint_count(),
464            2,
465            "Motor should have exactly 2 constraints for 6 DOF"
466        );
467
468        // Check flector has constraint(s)
469        let flector = entities.iter().find(|e| e.grades == vec![1, 3]).unwrap();
470        assert!(flector.has_constraints(), "Flector should have constraints");
471    }
472
473    #[test]
474    fn analyze_rotor_no_conflicts() {
475        let algebra = Algebra::euclidean(3);
476        let conflicts = analyze_constraints(&[0, 2], &algebra);
477        assert!(
478            conflicts.is_empty(),
479            "Rotor [0, 2] should have no conflicts"
480        );
481    }
482
483    #[test]
484    fn analyze_full_has_conflicts() {
485        let algebra = Algebra::euclidean(3);
486        let conflicts = analyze_constraints(&[0, 1, 2, 3], &algebra);
487        assert!(
488            !conflicts.is_empty(),
489            "Full multivector should have conflicts"
490        );
491
492        // Should find conflicts between grades that don't cancel
493        // e.g., (0, 1), (0, 3), (1, 2), (2, 3)
494        assert!(conflicts.iter().any(|c| c.grade_a == 0 && c.grade_b == 1));
495    }
496
497    #[test]
498    fn analyze_scalar_vector_conflict() {
499        let algebra = Algebra::euclidean(3);
500        let conflicts = analyze_constraints(&[0, 1], &algebra);
501        assert_eq!(conflicts.len(), 1);
502        assert_eq!(conflicts[0].grade_a, 0);
503        assert_eq!(conflicts[0].grade_b, 1);
504    }
505
506    #[test]
507    fn discover_pga_3d() {
508        let algebra = Algebra::pga(3); // 4D
509        let valid = discover_valid_combinations(&algebra);
510
511        // Scalar and pseudoscalar always satisfy
512        assert!(valid.contains(&vec![0]));
513        assert!(valid.contains(&vec![4]));
514
515        // Vectors and trivectors satisfy
516        assert!(valid.contains(&vec![1]));
517        assert!(valid.contains(&vec![3]));
518
519        // Bivectors do NOT satisfy in 4D (disjoint bivectors commute)
520        assert!(!valid.contains(&vec![2]));
521    }
522}