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}