Skip to main content

clifford_codegen/discovery/
constraints.rs

1//! Field constraint derivation for geometric entities.
2//!
3//! When a grade combination doesn't automatically satisfy geometric constraints,
4//! this module derives the field constraint expression that must hold.
5//!
6//! There are two types of constraints:
7//!
8//! 1. **Geometric Product Constraint**: `u * ũ = scalar`
9//!    For example, PGA bivectors (lines) have the constraint:
10//!    `e01*e23 + e02*e31 + e03*e12 = 0` (direction · moment = 0)
11//!
12//! 2. **Antiproduct Constraint**: `u ⊟ ũ̃ = antiscalar`
13//!    This is the dual constraint that must also be satisfied.
14
15use std::collections::HashMap;
16
17use crate::algebra::{
18    Algebra, Blade, ProductTable, antireverse_sign, blades_of_grades, grade, reverse_sign,
19};
20
21/// Derives the field constraint expression for a grade combination.
22///
23/// If the grade combination automatically satisfies geometric constraints,
24/// returns `None`. Otherwise, returns the constraint expression that must
25/// equal zero for the constraint to hold.
26///
27/// # Arguments
28///
29/// * `grades` - The grades present in the type
30/// * `algebra` - The algebra definition
31///
32/// # Returns
33///
34/// `None` if no constraint needed, or `Some(expression)` where the expression
35/// must equal zero. The expression uses blade names from the algebra.
36///
37/// # Example
38///
39/// ```
40/// use clifford_codegen::discovery::derive_field_constraint;
41/// use clifford_codegen::algebra::Algebra;
42///
43/// let algebra = Algebra::pga(3);
44///
45/// // PGA bivectors need a constraint
46/// let constraint = derive_field_constraint(&[2], &algebra);
47/// assert!(constraint.is_some());
48/// // Returns something like "e01*e23 + e02*e31 + e03*e12 = 0"
49/// ```
50pub fn derive_field_constraint(grades: &[usize], algebra: &Algebra) -> Option<String> {
51    let table = ProductTable::new(algebra);
52    let blades = blades_of_grades(algebra.dim(), grades);
53
54    // Collect non-scalar contributions from u * reverse(u)
55    // For each non-scalar output blade, collect the terms that contribute to it
56    let mut non_scalar_terms: HashMap<usize, Vec<(usize, usize, i32)>> = HashMap::new();
57
58    for (i, &a) in blades.iter().enumerate() {
59        for &b in &blades[i..] {
60            let ga = grade(a);
61            let gb = grade(b);
62            let rev_a = reverse_sign(ga);
63            let rev_b = reverse_sign(gb);
64
65            let (sign_ab, result_ab) = table.geometric(a, b);
66
67            if grade(result_ab) == 0 {
68                continue; // Scalar output, no constraint needed
69            }
70
71            if a == b {
72                // Same blade producing non-scalar - this is a fundamental problem
73                // that can't be solved with field constraints
74                if sign_ab != 0 {
75                    return None; // Can't derive a useful constraint
76                }
77            } else {
78                // Different blades producing non-scalar
79                let (sign_ba, _) = table.geometric(b, a);
80
81                // Total coefficient for result blade when terms don't cancel
82                let coef =
83                    i32::from(sign_ab) * i32::from(rev_b) + i32::from(sign_ba) * i32::from(rev_a);
84
85                if coef != 0 {
86                    // This pair contributes to a non-scalar result
87                    non_scalar_terms
88                        .entry(result_ab)
89                        .or_default()
90                        .push((a, b, coef));
91                }
92            }
93        }
94    }
95
96    if non_scalar_terms.is_empty() {
97        return None; // No constraint needed
98    }
99
100    // Build constraint expression
101    // For each non-scalar output blade, the sum of contributions must be zero
102    // We combine all into one constraint expression
103    let mut terms: Vec<String> = Vec::new();
104
105    for contributions in non_scalar_terms.values() {
106        for &(a, b, coef) in contributions {
107            let blade_a = Blade::from_index(a);
108            let blade_b = Blade::from_index(b);
109            let name_a = algebra.blade_index_name(blade_a);
110            let name_b = algebra.blade_index_name(blade_b);
111
112            let term = if coef == 1 {
113                format!("{}*{}", name_a, name_b)
114            } else if coef == -1 {
115                format!("-{}*{}", name_a, name_b)
116            } else {
117                // Both positive and negative coefficients with magnitude > 1
118                format!("{}*{}*{}", coef, name_a, name_b)
119            };
120            terms.push(term);
121        }
122    }
123
124    if terms.is_empty() {
125        return None;
126    }
127
128    // Join terms with + (handling leading -)
129    let mut expr = String::new();
130    for (i, term) in terms.iter().enumerate() {
131        if i == 0 {
132            expr.push_str(term);
133        } else if let Some(stripped) = term.strip_prefix('-') {
134            expr.push_str(" - ");
135            expr.push_str(stripped);
136        } else {
137            expr.push_str(" + ");
138            expr.push_str(term);
139        }
140    }
141
142    Some(format!("{} = 0", expr))
143}
144
145/// Derives the antiproduct field constraint expression for a grade combination.
146///
147/// The antiproduct constraint requires that `u ⊟ ũ̃` produces only an antiscalar
148/// (grade n). This function derives the field constraint that must hold for this
149/// to be true.
150///
151/// The antiproduct is computed as: `a ⊟ b = dual(dual(a) * dual(b))`
152///
153/// # Arguments
154///
155/// * `grades` - The grades present in the type
156/// * `algebra` - The algebra definition
157///
158/// # Returns
159///
160/// `None` if no constraint needed, or `Some(expression)` where the expression
161/// must equal zero.
162///
163/// # Example
164///
165/// ```
166/// use clifford_codegen::discovery::derive_antiproduct_constraint;
167/// use clifford_codegen::algebra::Algebra;
168///
169/// let algebra = Algebra::pga(3);
170///
171/// // PGA motors need an antiproduct constraint
172/// let constraint = derive_antiproduct_constraint(&[0, 2, 4], &algebra);
173/// // Returns the constraint expression if one is needed
174/// ```
175pub fn derive_antiproduct_constraint(grades: &[usize], algebra: &Algebra) -> Option<String> {
176    let table = ProductTable::new(algebra);
177    let dim = algebra.dim();
178    let blades = blades_of_grades(dim, grades);
179    let pseudoscalar = (1 << dim) - 1; // All bits set = e123...n
180
181    // Collect non-antiscalar contributions from u ⊟ antireverse(u)
182    // For each non-antiscalar output blade, collect the terms that contribute to it
183    let mut non_antiscalar_terms: HashMap<usize, Vec<(usize, usize, i32)>> = HashMap::new();
184
185    for (i, &a) in blades.iter().enumerate() {
186        for &b in &blades[i..] {
187            let ga = grade(a);
188            let gb = grade(b);
189            let antirev_a = antireverse_sign(ga, dim);
190            let antirev_b = antireverse_sign(gb, dim);
191
192            // Compute dual blades
193            let dual_a = pseudoscalar ^ a;
194            let dual_b = pseudoscalar ^ b;
195
196            // Product of duals
197            let (sign_ab, dual_result_ab) = table.geometric(dual_a, dual_b);
198            // Dual back to get antiproduct result
199            let result_ab = pseudoscalar ^ dual_result_ab;
200
201            if grade(result_ab) == dim {
202                continue; // Antiscalar output (grade n), no constraint needed
203            }
204
205            if a == b {
206                // Same blade producing non-antiscalar - fundamental problem
207                if sign_ab != 0 {
208                    return None; // Can't derive a useful constraint
209                }
210            } else {
211                // Different blades producing non-antiscalar
212                let (sign_ba, _) = table.geometric(dual_b, dual_a);
213
214                // Total coefficient for result blade when terms don't cancel
215                let coef = i32::from(sign_ab) * i32::from(antirev_b)
216                    + i32::from(sign_ba) * i32::from(antirev_a);
217
218                if coef != 0 {
219                    // This pair contributes to a non-antiscalar result
220                    non_antiscalar_terms
221                        .entry(result_ab)
222                        .or_default()
223                        .push((a, b, coef));
224                }
225            }
226        }
227    }
228
229    if non_antiscalar_terms.is_empty() {
230        return None; // No constraint needed
231    }
232
233    // Build constraint expression
234    let mut terms: Vec<String> = Vec::new();
235
236    for contributions in non_antiscalar_terms.values() {
237        for &(a, b, coef) in contributions {
238            let blade_a = Blade::from_index(a);
239            let blade_b = Blade::from_index(b);
240            let name_a = algebra.blade_index_name(blade_a);
241            let name_b = algebra.blade_index_name(blade_b);
242
243            let term = if coef == 1 {
244                format!("{}*{}", name_a, name_b)
245            } else if coef == -1 {
246                format!("-{}*{}", name_a, name_b)
247            } else {
248                format!("{}*{}*{}", coef, name_a, name_b)
249            };
250            terms.push(term);
251        }
252    }
253
254    if terms.is_empty() {
255        return None;
256    }
257
258    // Join terms with + (handling leading -)
259    let mut expr = String::new();
260    for (i, term) in terms.iter().enumerate() {
261        if i == 0 {
262            expr.push_str(term);
263        } else if let Some(stripped) = term.strip_prefix('-') {
264            expr.push_str(" - ");
265            expr.push_str(stripped);
266        } else {
267            expr.push_str(" + ");
268            expr.push_str(term);
269        }
270    }
271
272    Some(format!("{} = 0", expr))
273}
274
275/// Derives the blade constraint expression for a grade combination.
276///
277/// A k-vector B is a simple blade (can be written as v₁ ∧ v₂ ∧ ... ∧ vₖ) if and only if
278/// B ∧ B = 0. This function derives the constraint equations that must hold.
279///
280/// This is the constraint used by CGA for geometric primitives like dipoles, circles,
281/// and spheres. It ensures the element represents a valid geometric object.
282///
283/// # Arguments
284///
285/// * `grades` - The grades present in the type (typically a single grade for blades)
286/// * `algebra` - The algebra definition
287///
288/// # Returns
289///
290/// A vector of constraint expressions, one for each non-zero component of B ∧ B.
291/// Each expression must equal zero for the element to be a valid blade.
292///
293/// # Example
294///
295/// ```
296/// use clifford_codegen::discovery::derive_blade_constraint;
297/// use clifford_codegen::algebra::Algebra;
298///
299/// // CGA dipole (grade 2 in 5D) needs blade constraints
300/// let algebra = Algebra::new(4, 1, 0); // Cl(4,1,0)
301/// let constraints = derive_blade_constraint(&[2], &algebra);
302/// assert_eq!(constraints.len(), 5); // 5 grade-4 components
303/// ```
304pub fn derive_blade_constraint(grades: &[usize], algebra: &Algebra) -> Vec<String> {
305    let table = ProductTable::new(algebra);
306    let dim = algebra.dim();
307    let blades = blades_of_grades(dim, grades);
308
309    // B ∧ B produces grade 2k for a k-vector
310    // Collect terms for each output blade
311    let mut output_terms: HashMap<usize, Vec<(usize, usize, i32)>> = HashMap::new();
312
313    for (i, &a) in blades.iter().enumerate() {
314        for &b in &blades[i..] {
315            let (sign, result) = table.exterior(a, b);
316            if sign == 0 {
317                continue;
318            }
319
320            // For B ∧ B, we get 2*b_a*b_b for a ≠ b (symmetric sum)
321            let coef = if a == b {
322                i32::from(sign)
323            } else {
324                2 * i32::from(sign)
325            };
326
327            output_terms.entry(result).or_default().push((a, b, coef));
328        }
329    }
330
331    // Build constraint expressions
332    let mut constraints = Vec::new();
333
334    for (result_blade, terms) in output_terms {
335        if terms.is_empty() {
336            continue;
337        }
338
339        let mut expr_parts: Vec<String> = Vec::new();
340        for (a, b, coef) in terms {
341            let blade_a = Blade::from_index(a);
342            let blade_b = Blade::from_index(b);
343            let name_a = algebra.blade_index_name(blade_a);
344            let name_b = algebra.blade_index_name(blade_b);
345
346            let term = if a == b {
347                // Diagonal term: coef * name²
348                if coef == 1 {
349                    format!("{}^2", name_a)
350                } else if coef == -1 {
351                    format!("-{}^2", name_a)
352                } else {
353                    format!("{}*{}^2", coef, name_a)
354                }
355            } else {
356                // Off-diagonal term: coef * name_a * name_b
357                if coef == 1 {
358                    format!("{}*{}", name_a, name_b)
359                } else if coef == -1 {
360                    format!("-{}*{}", name_a, name_b)
361                } else {
362                    format!("{}*{}*{}", coef, name_a, name_b)
363                }
364            };
365            expr_parts.push(term);
366        }
367
368        // Join terms
369        let mut expr = String::new();
370        for (i, part) in expr_parts.iter().enumerate() {
371            if i == 0 {
372                expr.push_str(part);
373            } else if let Some(stripped) = part.strip_prefix('-') {
374                expr.push_str(" - ");
375                expr.push_str(stripped);
376            } else {
377                expr.push_str(" + ");
378                expr.push_str(part);
379            }
380        }
381
382        // Add blade name as comment
383        let result_name = algebra.blade_index_name(Blade::from_index(result_blade));
384        constraints.push(format!("{} = 0  // {} component", expr, result_name));
385    }
386
387    constraints
388}
389
390/// Derives the null constraint expression for a grade combination.
391///
392/// A null element has zero norm: `v * ṽ = 0`. This function derives the constraint
393/// equations that must hold for an element to be null.
394///
395/// In CGA, points are null vectors (grade 1 with v * ṽ = 0). This constraint is
396/// essential for representing actual geometric points vs general vectors.
397///
398/// # Arguments
399///
400/// * `grades` - The grades present in the type
401/// * `algebra` - The algebra definition
402///
403/// # Returns
404///
405/// A vector of constraint expressions. For the null constraint, this includes:
406/// - The scalar part of v * ṽ must be zero
407/// - Any non-scalar parts must also be zero (same as versor constraint)
408///
409/// # Example
410///
411/// ```
412/// use clifford_codegen::discovery::derive_null_constraint;
413/// use clifford_codegen::algebra::Algebra;
414///
415/// // CGA point (grade 1) needs null constraint
416/// let algebra = Algebra::new(4, 1, 0); // Cl(4,1,0)
417/// let constraints = derive_null_constraint(&[1], &algebra);
418/// // Returns constraint that x² + y² + z² + w² - u² = 0 (null vector)
419/// ```
420pub fn derive_null_constraint(grades: &[usize], algebra: &Algebra) -> Vec<String> {
421    let table = ProductTable::new(algebra);
422    let blades = blades_of_grades(algebra.dim(), grades);
423
424    // Collect ALL contributions from v * reverse(v), including scalar
425    let mut all_terms: HashMap<usize, Vec<(usize, usize, i32)>> = HashMap::new();
426
427    for (i, &a) in blades.iter().enumerate() {
428        for &b in &blades[i..] {
429            let ga = grade(a);
430            let gb = grade(b);
431            let rev_a = reverse_sign(ga);
432            let rev_b = reverse_sign(gb);
433
434            let (sign_ab, result_ab) = table.geometric(a, b);
435
436            if sign_ab == 0 {
437                continue;
438            }
439
440            if a == b {
441                // Diagonal term: a * reverse(a) = sign * |a|²
442                let coef = i32::from(sign_ab) * i32::from(rev_a);
443                all_terms.entry(result_ab).or_default().push((a, a, coef));
444            } else {
445                // Off-diagonal terms
446                let (sign_ba, _) = table.geometric(b, a);
447                let coef =
448                    i32::from(sign_ab) * i32::from(rev_b) + i32::from(sign_ba) * i32::from(rev_a);
449
450                if coef != 0 {
451                    all_terms.entry(result_ab).or_default().push((a, b, coef));
452                }
453            }
454        }
455    }
456
457    // Build constraint expressions for ALL output grades (including scalar = 0)
458    let mut constraints = Vec::new();
459
460    for (result_blade, terms) in all_terms {
461        if terms.is_empty() {
462            continue;
463        }
464
465        let mut expr_parts: Vec<String> = Vec::new();
466        for (a, b, coef) in terms {
467            let blade_a = Blade::from_index(a);
468            let name_a = algebra.blade_index_name(blade_a);
469
470            let term = if a == b {
471                // Squared term
472                if coef == 1 {
473                    format!("{}²", name_a)
474                } else if coef == -1 {
475                    format!("-{}²", name_a)
476                } else {
477                    format!("{}*{}²", coef, name_a)
478                }
479            } else {
480                let blade_b = Blade::from_index(b);
481                let name_b = algebra.blade_index_name(blade_b);
482                if coef == 1 {
483                    format!("{}*{}", name_a, name_b)
484                } else if coef == -1 {
485                    format!("-{}*{}", name_a, name_b)
486                } else {
487                    format!("{}*{}*{}", coef, name_a, name_b)
488                }
489            };
490            expr_parts.push(term);
491        }
492
493        // Join terms
494        let mut expr = String::new();
495        for (i, part) in expr_parts.iter().enumerate() {
496            if i == 0 {
497                expr.push_str(part);
498            } else if let Some(stripped) = part.strip_prefix('-') {
499                expr.push_str(" - ");
500                expr.push_str(stripped);
501            } else {
502                expr.push_str(" + ");
503                expr.push_str(part);
504            }
505        }
506
507        let result_grade = grade(result_blade);
508        let grade_name = if result_grade == 0 {
509            "scalar (norm)".to_string()
510        } else {
511            let result_name = algebra.blade_index_name(Blade::from_index(result_blade));
512            result_name.to_string()
513        };
514
515        constraints.push(format!("{} = 0  // {} component", expr, grade_name));
516    }
517
518    constraints
519}
520
521/// Checks if a grade combination can satisfy constraints with field constraints.
522///
523/// Some grade combinations fundamentally cannot satisfy geometric constraints
524/// (e.g., a blade whose square is non-scalar). This function distinguishes between:
525/// - Combinations that automatically satisfy constraints (returns `(true, None)`)
526/// - Combinations that can satisfy with field constraints (returns `(true, Some(constraint))`)
527/// - Combinations that cannot satisfy constraints (returns `(false, None)`)
528///
529/// # Arguments
530///
531/// * `grades` - The grades present in the type
532/// * `algebra` - The algebra definition
533///
534/// # Returns
535///
536/// `(satisfiable, constraint)` where:
537/// - `satisfiable` is true if constraints can be satisfied (with or without field constraints)
538/// - `constraint` is the field constraint expression, if one is needed
539pub fn can_satisfy_constraints(grades: &[usize], algebra: &Algebra) -> (bool, Option<String>) {
540    let table = ProductTable::new(algebra);
541    let blades = blades_of_grades(algebra.dim(), grades);
542
543    // Check: are there any same-blade non-scalar products?
544    // This indicates a fundamental problem that field constraints can't solve
545    // (a blade whose square is non-scalar violates the constraint regardless of coefficient)
546    for &a in &blades {
547        let (sign_aa, result_aa) = table.geometric(a, a);
548        if sign_aa != 0 && grade(result_aa) != 0 {
549            return (false, None); // Fundamental violation
550        }
551    }
552
553    // All other combinations can potentially satisfy constraints with field constraints
554    // Derive the constraint expression (if any is needed)
555    let constraint = derive_field_constraint(grades, algebra);
556    (true, constraint)
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn euclidean_3d_no_constraints() {
565        let algebra = Algebra::euclidean(3);
566
567        // Single grades need no constraints
568        assert_eq!(derive_field_constraint(&[0], &algebra), None);
569        assert_eq!(derive_field_constraint(&[1], &algebra), None);
570        assert_eq!(derive_field_constraint(&[2], &algebra), None);
571        assert_eq!(derive_field_constraint(&[3], &algebra), None);
572
573        // Even/odd subalgebras need no constraints
574        assert_eq!(derive_field_constraint(&[0, 2], &algebra), None);
575        assert_eq!(derive_field_constraint(&[1, 3], &algebra), None);
576    }
577
578    #[test]
579    fn pga_3d_bivector_constraint() {
580        let algebra = Algebra::pga(3);
581
582        // Bivectors need a constraint
583        let constraint = derive_field_constraint(&[2], &algebra);
584        assert!(constraint.is_some());
585
586        let expr = constraint.unwrap();
587        // Should contain products of bivector blades
588        assert!(expr.contains("="));
589    }
590
591    #[test]
592    fn pga_3d_satisfiability() {
593        let algebra = Algebra::pga(3);
594
595        // Scalar - satisfiable, no constraint
596        let (sat, constr) = can_satisfy_constraints(&[0], &algebra);
597        assert!(sat);
598        assert!(constr.is_none());
599
600        // Bivector - satisfiable with constraint
601        let (sat, constr) = can_satisfy_constraints(&[2], &algebra);
602        assert!(sat);
603        assert!(constr.is_some());
604
605        // Scalar + Vector - satisfiable with constraint (constraint requires s * v_i = 0)
606        let (sat, constr) = can_satisfy_constraints(&[0, 1], &algebra);
607        assert!(sat);
608        assert!(constr.is_some());
609
610        // Vector + Trivector (Flector) - satisfiable with constraint
611        let (sat, constr) = can_satisfy_constraints(&[1, 3], &algebra);
612        assert!(sat);
613        assert!(constr.is_some());
614    }
615
616    #[test]
617    fn cga_dipole_constraints_analysis() {
618        // CGA: Cl(4,1,0) - 4 positive, 1 negative
619        let algebra = Algebra::new(4, 1, 0);
620
621        let geometric = derive_field_constraint(&[2], &algebra);
622        let _antiproduct = derive_antiproduct_constraint(&[2], &algebra);
623
624        // Print for analysis
625        eprintln!("\n=== CGA Dipole Constraint Analysis ===");
626        eprintln!("Field mapping (CGA wiki convention):");
627        eprintln!("  m (moment):   e12=mx, e13=my, e23=mz");
628        eprintln!("  v (velocity): e14=vx, e24=vy, e34=vz");
629        eprintln!("  p (position): e15=px, e25=py, e35=pz, e45=pw");
630        eprintln!();
631
632        if let Some(ref c) = geometric {
633            eprintln!("Inferred geometric constraint (d * d̃ = scalar):");
634            eprintln!("  {}", c);
635        }
636        eprintln!();
637        eprintln!("CGA wiki constraints:");
638        eprintln!("  1. p × v - pw*m = 0");
639        eprintln!("     Expands to:");
640        eprintln!("       py*vz - pz*vy - pw*mx = 0  (e25*e34 - e35*e24 - e45*e12)");
641        eprintln!("       pz*vx - px*vz - pw*my = 0  (e35*e14 - e15*e34 - e45*e13)");
642        eprintln!("       px*vy - py*vx - pw*mz = 0  (e15*e24 - e25*e14 - e45*e23)");
643        eprintln!("  2. p · m = 0  →  px*mx + py*my + pz*mz = 0  (e15*e12 + e25*e13 + e35*e23)");
644        eprintln!("  3. v · m = 0  →  vx*mx + vy*my + vz*mz = 0  (e14*e12 + e24*e13 + e34*e23)");
645        eprintln!("==========================================\n");
646
647        // The inferred constraint is for the norm, which is different from geometric validity
648        assert!(
649            geometric.is_some(),
650            "CGA dipoles should have a geometric constraint"
651        );
652    }
653
654    #[test]
655    fn blade_constraint_across_algebras() {
656        // Test which algebras need blade constraints for bivectors
657        eprintln!("\n=== Blade Constraints (B ∧ B = 0) Across Algebras ===\n");
658
659        let test_cases = [
660            ("Euclidean 2D", Algebra::euclidean(2), 2),
661            ("Euclidean 3D", Algebra::euclidean(3), 2),
662            ("Euclidean 4D", Algebra::euclidean(4), 2),
663            ("PGA 2D", Algebra::pga(2), 2),
664            ("PGA 3D", Algebra::pga(3), 2),
665            ("CGA 3D (Cl(4,1,0))", Algebra::new(4, 1, 0), 2),
666            ("Minkowski (Cl(3,1,0))", Algebra::new(3, 1, 0), 2),
667        ];
668
669        for (name, algebra, grade) in &test_cases {
670            let constraints = derive_blade_constraint(&[*grade], algebra);
671            let num_blades = blades_of_grades(algebra.dim(), &[*grade]).len();
672
673            eprintln!(
674                "{}: {} grade-{} blades → {} blade constraints",
675                name,
676                num_blades,
677                grade,
678                constraints.len()
679            );
680
681            if !constraints.is_empty() {
682                for c in &constraints {
683                    eprintln!("    {}", c);
684                }
685            }
686        }
687
688        eprintln!("\n=== Summary ===");
689        eprintln!("Blade constraints needed when dim > 2*grade:");
690        eprintln!("  - 2D: bivectors (3 components) - no constraint (dim=2, 2*2=4 > 2)");
691        eprintln!("  - 3D: bivectors (3 components) - no constraint (all bivectors are simple)");
692        eprintln!("  - 4D+: bivectors (6+ components) - constraints needed");
693        eprintln!("=============================================\n");
694    }
695
696    #[test]
697    fn versor_constraint_across_algebras() {
698        // Test which algebras need versor constraints for even subalgebra
699        eprintln!("\n=== Versor Constraints (v * ṽ = scalar) Across Algebras ===\n");
700
701        let test_cases = [
702            (
703                "Euclidean 3D Rotor [0,2]",
704                Algebra::euclidean(3),
705                vec![0, 2],
706            ),
707            ("PGA 3D Motor [0,2,4]", Algebra::pga(3), vec![0, 2, 4]),
708            ("CGA 3D Motor [0,2,4]", Algebra::new(4, 1, 0), vec![0, 2, 4]),
709        ];
710
711        for (name, algebra, grades) in &test_cases {
712            let constraint = derive_field_constraint(grades, algebra);
713
714            eprintln!("{}: {:?}", name, grades);
715            if let Some(c) = constraint {
716                eprintln!("    Constraint: {}", c);
717            } else {
718                eprintln!("    No constraint needed (automatically satisfies)");
719            }
720        }
721        eprintln!("=============================================\n");
722    }
723
724    #[test]
725    fn cga_blade_constraint_matches_wiki() {
726        // Verify CGA dipole blade constraints match CGA wiki
727        let algebra = Algebra::new(4, 1, 0);
728        let constraints = derive_blade_constraint(&[2], &algebra);
729
730        eprintln!("\n=== CGA Dipole Blade Constraints ===");
731        eprintln!("Field mapping: mx=e12, my=e13, mz=e23, vx=e14, vy=e24, vz=e34,");
732        eprintln!("               px=e15, py=e25, pz=e35, pw=e45\n");
733
734        for c in &constraints {
735            eprintln!("  {}", c);
736        }
737
738        eprintln!("\nCGA wiki equivalent:");
739        eprintln!("  1. p × v - pw·m = 0 (3 equations from e1245, e1345, e2345)");
740        eprintln!("  2. p · m = 0        (from e1235)");
741        eprintln!("  3. v · m = 0        (from e1234)");
742        eprintln!("==========================================\n");
743
744        assert_eq!(
745            constraints.len(),
746            5,
747            "CGA dipole should have 5 blade constraints"
748        );
749    }
750
751    #[test]
752    fn null_constraint_cga_point() {
753        // CGA points are null vectors: v * ṽ = 0
754        let algebra = Algebra::new(4, 1, 0);
755        let constraints = derive_null_constraint(&[1], &algebra);
756
757        eprintln!("\n=== CGA Point Null Constraint ===");
758        eprintln!("A point in CGA is a null vector (v * ṽ = 0)");
759        eprintln!("Components: x=e1, y=e2, z=e3, w=e4, u=e5\n");
760
761        for c in &constraints {
762            eprintln!("  {}", c);
763        }
764
765        eprintln!("\nExpected: x² + y² + z² + w² - u² = 0");
766        eprintln!("(indefinite signature: e1²=e2²=e3²=e4²=+1, e5²=-1)");
767        eprintln!("==========================================\n");
768
769        // Should have 1 constraint (the scalar/norm part)
770        assert!(
771            !constraints.is_empty(),
772            "CGA point should have null constraint"
773        );
774    }
775
776    #[test]
777    fn null_constraint_euclidean() {
778        // Euclidean vectors: v * ṽ = |v|² (positive definite, no null vectors except 0)
779        let algebra = Algebra::euclidean(3);
780        let constraints = derive_null_constraint(&[1], &algebra);
781
782        eprintln!("\n=== Euclidean 3D Vector Null Constraint ===");
783        for c in &constraints {
784            eprintln!("  {}", c);
785        }
786        eprintln!("Expected: x² + y² + z² = 0 (only zero vector is null)");
787        eprintln!("==========================================\n");
788
789        assert!(!constraints.is_empty());
790    }
791
792    #[test]
793    fn null_constraint_minkowski() {
794        // Minkowski vectors can be null (lightlike)
795        let algebra = Algebra::new(3, 1, 0); // Cl(3,1,0)
796        let constraints = derive_null_constraint(&[1], &algebra);
797
798        eprintln!("\n=== Minkowski Vector Null Constraint ===");
799        eprintln!("Lightlike vectors satisfy: x² + y² + z² - t² = 0\n");
800
801        for c in &constraints {
802            eprintln!("  {}", c);
803        }
804        eprintln!("==========================================\n");
805
806        assert!(!constraints.is_empty());
807    }
808
809    #[test]
810    fn cga_circle_blade_constraint_matches_wiki() {
811        // Verify CGA Circle (grade 3) blade constraints match CGA wiki
812        // Wiki: https://conformalgeometricalgebra.org/wiki/index.php?title=Circle
813        let algebra = Algebra::new(4, 1, 0);
814        let constraints = derive_blade_constraint(&[3], &algebra);
815
816        eprintln!("\n=== CGA Circle Blade Constraints ===");
817        eprintln!("Field mapping from conformal3.toml:");
818        eprintln!("  g (plane normal): gw=e123, gz=e124, gy=e134, gx=e234");
819        eprintln!("  m (center):       mz=e125, my=e135, mx=e235");
820        eprintln!("  v (moment):       vx=e145, vy=e245, vz=e345\n");
821
822        eprintln!("Derived blade constraints (k ∧ k = 0):");
823        for c in &constraints {
824            eprintln!("  {}", c);
825        }
826
827        eprintln!("\nCGA wiki Circle constraints:");
828        eprintln!("  1. g × m - gw*v = 0 (3 equations)");
829        eprintln!("     gy*mz - gz*my - gw*vx = 0");
830        eprintln!("     gz*mx - gx*mz - gw*vy = 0");
831        eprintln!("     gx*my - gy*mx - gw*vz = 0");
832        eprintln!("  2. g · v = 0");
833        eprintln!("     gx*vx + gy*vy + gz*vz = 0");
834        eprintln!("  3. v · m = 0");
835        eprintln!("     vx*mx + vy*my + vz*mz = 0");
836        eprintln!("==========================================\n");
837
838        // Circle is a trivector (grade 3), so k ∧ k produces grade 6
839        // In 5D, grade 6 = 0 (doesn't exist), so no blade constraints!
840        // But the wiki constraints DO come from the VERSOR constraint (k * k̃ = scalar)
841        eprintln!("Note: In 5D CGA, grade 3 ∧ grade 3 = grade 6, which is empty.");
842        eprintln!("However, the wiki constraints come from the VERSOR constraint:");
843        eprintln!("k * k̃ for a trivector gives grade 0, 2, 4 components.");
844        eprintln!("The grade 4 components must vanish → gives 5 constraints!\n");
845
846        // Grade 3 in 5D: there are C(5,3) = 10 blades
847        // Grade 6 in 5D: there are C(5,6) = 0 blades (doesn't exist)
848        // So no blade constraints are needed for grade 3 in 5D
849        assert!(
850            constraints.is_empty(),
851            "CGA Circle (grade 3) should have no blade constraints (grade 6 doesn't exist in 5D)"
852        );
853    }
854}
855
856/// Tests verifying Circle constraints match CGA wiki
857#[cfg(test)]
858mod circle_constraint_tests {
859    use super::*;
860    use std::collections::HashMap;
861
862    #[test]
863    fn circle_5_constraints_match_wiki() {
864        // Show the 5 constraints correspond to CGA wiki constraints
865        let algebra = Algebra::new(4, 1, 0);
866        let table = ProductTable::new(&algebra);
867        let blades = blades_of_grades(5, &[3]);
868
869        eprintln!("\n=== Circle: 5 Versor Constraints Match Wiki ===\n");
870        eprintln!("Field mapping:");
871        eprintln!("  g: gw=e123, gz=e124, gy=e134, gx=e234");
872        eprintln!("  m: mz=e125, my=e135, mx=e235");
873        eprintln!("  v: vx=e145, vy=e245, vz=e345\n");
874
875        // Group terms by output blade
876        let mut by_blade: HashMap<usize, Vec<(String, String, i32)>> = HashMap::new();
877
878        for (i, &a) in blades.iter().enumerate() {
879            for &b in &blades[i..] {
880                let rev_b = reverse_sign(3);
881                let rev_a = reverse_sign(3);
882
883                let (sign_ab, result) = table.geometric(a, b);
884                if sign_ab == 0 || grade(result) == 0 {
885                    continue;
886                }
887
888                let name_a = algebra.blade_index_name(Blade::from_index(a));
889                let name_b = algebra.blade_index_name(Blade::from_index(b));
890
891                let coef = if a == b {
892                    i32::from(sign_ab) * i32::from(rev_a)
893                } else {
894                    let (sign_ba, _) = table.geometric(b, a);
895                    i32::from(sign_ab) * i32::from(rev_b) + i32::from(sign_ba) * i32::from(rev_a)
896                };
897
898                if coef != 0 {
899                    by_blade.entry(result).or_default().push((
900                        name_a.to_string(),
901                        name_b.to_string(),
902                        coef,
903                    ));
904                }
905            }
906        }
907
908        // Map blade to field name
909        let blade_to_field: HashMap<&str, &str> = [
910            ("e123", "gw"),
911            ("e124", "gz"),
912            ("e134", "gy"),
913            ("e234", "gx"),
914            ("e125", "mz"),
915            ("e135", "my"),
916            ("e235", "mx"),
917            ("e145", "vx"),
918            ("e245", "vy"),
919            ("e345", "vz"),
920        ]
921        .into_iter()
922        .collect();
923
924        // Print each constraint with wiki interpretation
925        let wiki_labels: HashMap<&str, &str> = [
926            ("e1234", "v · m = 0"),
927            ("e1235", "g · v = 0"),
928            ("e1245", "(g × m)_z - gw*vz = 0"),
929            ("e1345", "(g × m)_y - gw*vy = 0"),
930            ("e2345", "(g × m)_x - gw*vx = 0"),
931        ]
932        .into_iter()
933        .collect();
934
935        for (&result, terms) in &by_blade {
936            let result_name = algebra.blade_index_name(Blade::from_index(result));
937            let wiki_label = wiki_labels.get(result_name.as_str()).unwrap_or(&"unknown");
938
939            eprintln!("{} constraint (wiki: {}):", result_name, wiki_label);
940
941            let mut field_terms: Vec<String> = Vec::new();
942            for (a, b, coef) in terms {
943                let fa = blade_to_field
944                    .get(a.as_str())
945                    .copied()
946                    .unwrap_or(a.as_str());
947                let fb = blade_to_field
948                    .get(b.as_str())
949                    .copied()
950                    .unwrap_or(b.as_str());
951                if *coef > 0 {
952                    field_terms.push(format!("+{}*{}*{}", coef, fa, fb));
953                } else {
954                    field_terms.push(format!("{}*{}*{}", coef, fa, fb));
955                }
956            }
957            eprintln!("  {}", field_terms.join(" "));
958            eprintln!();
959        }
960
961        eprintln!("==========================================\n");
962
963        // Verify we have exactly 5 constraints
964        assert_eq!(
965            by_blade.len(),
966            5,
967            "Should have exactly 5 grade-4 output blades"
968        );
969    }
970}