1use std::collections::HashMap;
16
17use crate::algebra::{
18 Algebra, Blade, ProductTable, antireverse_sign, blades_of_grades, grade, reverse_sign,
19};
20
21pub 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 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; }
70
71 if a == b {
72 if sign_ab != 0 {
75 return None; }
77 } else {
78 let (sign_ba, _) = table.geometric(b, a);
80
81 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 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; }
99
100 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 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 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
145pub 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; 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 let dual_a = pseudoscalar ^ a;
194 let dual_b = pseudoscalar ^ b;
195
196 let (sign_ab, dual_result_ab) = table.geometric(dual_a, dual_b);
198 let result_ab = pseudoscalar ^ dual_result_ab;
200
201 if grade(result_ab) == dim {
202 continue; }
204
205 if a == b {
206 if sign_ab != 0 {
208 return None; }
210 } else {
211 let (sign_ba, _) = table.geometric(dual_b, dual_a);
213
214 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 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; }
232
233 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 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
275pub 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 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 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 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 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 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 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 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
390pub 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 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 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 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 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 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 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
521pub 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 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); }
551 }
552
553 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 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 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 let constraint = derive_field_constraint(&[2], &algebra);
584 assert!(constraint.is_some());
585
586 let expr = constraint.unwrap();
587 assert!(expr.contains("="));
589 }
590
591 #[test]
592 fn pga_3d_satisfiability() {
593 let algebra = Algebra::pga(3);
594
595 let (sat, constr) = can_satisfy_constraints(&[0], &algebra);
597 assert!(sat);
598 assert!(constr.is_none());
599
600 let (sat, constr) = can_satisfy_constraints(&[2], &algebra);
602 assert!(sat);
603 assert!(constr.is_some());
604
605 let (sat, constr) = can_satisfy_constraints(&[0, 1], &algebra);
607 assert!(sat);
608 assert!(constr.is_some());
609
610 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 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 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 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 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 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 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 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 assert!(
771 !constraints.is_empty(),
772 "CGA point should have null constraint"
773 );
774 }
775
776 #[test]
777 fn null_constraint_euclidean() {
778 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 let algebra = Algebra::new(3, 1, 0); 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 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 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 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#[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 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 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 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 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 assert_eq!(
965 by_blade.len(),
966 5,
967 "Should have exactly 5 grade-4 output blades"
968 );
969 }
970}