1use crate::algebra::{
12 Algebra, ProductTable, binomial, blades_of_grades, geometric_grades, grade,
13 left_contraction_grade, outer_grade,
14};
15use std::collections::BTreeSet;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum ProductType {
20 Geometric,
22 Exterior,
24 LeftContraction,
26 RightContraction,
28 Regressive,
30 Scalar,
32 Antigeometric,
34 Antiscalar,
36 BulkContraction,
38 WeightContraction,
40 BulkExpansion,
42 WeightExpansion,
44 Dot,
46 Antidot,
48 Project,
50 Antiproject,
52}
53
54impl ProductType {
55 pub fn all() -> &'static [ProductType] {
57 &[
58 ProductType::Geometric,
59 ProductType::Exterior,
60 ProductType::LeftContraction,
61 ProductType::RightContraction,
62 ProductType::Regressive,
63 ProductType::Scalar,
64 ProductType::Antigeometric,
65 ProductType::Antiscalar,
66 ProductType::BulkContraction,
67 ProductType::WeightContraction,
68 ProductType::BulkExpansion,
69 ProductType::WeightExpansion,
70 ProductType::Dot,
71 ProductType::Antidot,
72 ProductType::Project,
73 ProductType::Antiproject,
74 ]
75 }
76
77 pub fn toml_name(&self) -> &'static str {
79 match self {
80 ProductType::Geometric => "geometric",
81 ProductType::Exterior => "exterior",
82 ProductType::LeftContraction => "left_contraction",
83 ProductType::RightContraction => "right_contraction",
84 ProductType::Regressive => "regressive",
85 ProductType::Scalar => "scalar",
86 ProductType::Antigeometric => "antigeometric",
87 ProductType::Antiscalar => "antiscalar",
88 ProductType::BulkContraction => "bulk_contraction",
89 ProductType::WeightContraction => "weight_contraction",
90 ProductType::BulkExpansion => "bulk_expansion",
91 ProductType::WeightExpansion => "weight_expansion",
92 ProductType::Dot => "dot",
93 ProductType::Antidot => "antidot",
94 ProductType::Project => "project",
95 ProductType::Antiproject => "antiproject",
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ProductResult {
103 pub output_grades: Vec<usize>,
106 pub matching_entity: Option<String>,
108 pub is_zero: bool,
110}
111
112#[derive(Debug, Clone)]
116pub struct EntityBladeSet {
117 pub name: String,
119 pub blades: BTreeSet<usize>,
121 pub grades: Vec<usize>,
123 pub is_sparse: bool,
125}
126
127impl EntityBladeSet {
128 pub fn new(name: String, blades: impl IntoIterator<Item = usize>) -> Self {
130 let blades: BTreeSet<usize> = blades.into_iter().collect();
131 let grades: Vec<usize> = blades
132 .iter()
133 .map(|b| b.count_ones() as usize)
134 .collect::<BTreeSet<_>>()
135 .into_iter()
136 .collect();
137 let is_sparse = !Self::spans_all_grades(&blades, &grades);
138 Self {
139 name,
140 blades,
141 grades,
142 is_sparse,
143 }
144 }
145
146 pub fn from_grades(name: String, grades: Vec<usize>, dim: usize) -> Self {
150 let blades = blades_of_grades(dim, &grades).into_iter().collect();
151 Self {
152 name,
153 blades,
154 grades,
155 is_sparse: false,
156 }
157 }
158
159 fn spans_all_grades(blades: &BTreeSet<usize>, grades: &[usize]) -> bool {
161 let max_blade = blades.iter().max().copied().unwrap_or(0);
163 let dim = (max_blade as f64).log2().ceil() as usize;
164 if dim == 0 && max_blade > 0 {
165 return false;
166 }
167
168 for &grade in grades {
169 let expected_count = binomial(dim.max(1), grade);
170 let actual_count = blades
171 .iter()
172 .filter(|b| b.count_ones() as usize == grade)
173 .count();
174 if actual_count != expected_count {
175 return false;
176 }
177 }
178 true
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct BladeProductResult {
185 pub output_blades: BTreeSet<usize>,
187 pub output_grades: Vec<usize>,
189 pub matching_entity: Option<String>,
191 pub is_zero: bool,
193}
194
195pub fn infer_output_blades(
212 lhs_blades: &BTreeSet<usize>,
213 rhs_blades: &BTreeSet<usize>,
214 product_type: ProductType,
215 algebra: &Algebra,
216 table: &ProductTable,
217) -> BTreeSet<usize> {
218 let mut output_set = BTreeSet::new();
219
220 for &a in lhs_blades {
221 for &b in rhs_blades {
222 let (sign, result) = match product_type {
224 ProductType::Regressive => table.regressive(a, b),
225 ProductType::Exterior => table.exterior(a, b),
226 ProductType::Antigeometric | ProductType::Antiscalar => table.antiproduct(a, b),
227 ProductType::BulkContraction => table.bulk_contraction(a, b),
228 ProductType::WeightContraction => table.weight_contraction(a, b),
229 ProductType::BulkExpansion => table.bulk_expansion(a, b),
230 ProductType::WeightExpansion => table.weight_expansion(a, b),
231 ProductType::Dot => table.dot(a, b),
232 ProductType::Antidot => table.antidot(a, b),
233 ProductType::Project => table.project(a, b),
234 ProductType::Antiproject => table.antiproject(a, b),
235 _ => table.geometric(a, b),
236 };
237
238 if sign == 0 {
239 continue;
240 }
241
242 let ga = grade(a);
243 let gb = grade(b);
244 let result_grade = grade(result);
245
246 let include = match product_type {
248 ProductType::Geometric => true,
249 ProductType::Exterior => result_grade == ga + gb,
250 ProductType::LeftContraction => ga <= gb && result_grade == gb - ga,
251 ProductType::RightContraction => gb <= ga && result_grade == ga - gb,
252 ProductType::Regressive => true,
253 ProductType::Scalar => result_grade == 0,
254 ProductType::Antigeometric => true,
255 ProductType::Antiscalar => result_grade == algebra.dim(),
256 ProductType::BulkContraction
257 | ProductType::WeightContraction
258 | ProductType::BulkExpansion
259 | ProductType::WeightExpansion => true,
260 ProductType::Dot => ga == gb && result_grade == 0,
261 ProductType::Antidot => ga == gb && result_grade == 0,
262 ProductType::Project | ProductType::Antiproject => true,
263 };
264
265 if include {
266 output_set.insert(result);
267 }
268 }
269 }
270
271 output_set
272}
273
274pub fn infer_product_blades(
292 lhs: &EntityBladeSet,
293 rhs: &EntityBladeSet,
294 product_type: ProductType,
295 known_entities: &[EntityBladeSet],
296 algebra: &Algebra,
297 table: &ProductTable,
298) -> BladeProductResult {
299 let output_blades = infer_output_blades(&lhs.blades, &rhs.blades, product_type, algebra, table);
300
301 if output_blades.is_empty() {
302 return BladeProductResult {
303 output_blades: BTreeSet::new(),
304 output_grades: vec![],
305 matching_entity: None,
306 is_zero: true,
307 };
308 }
309
310 let output_grades: Vec<usize> = output_blades
312 .iter()
313 .map(|b| b.count_ones() as usize)
314 .collect::<BTreeSet<_>>()
315 .into_iter()
316 .collect();
317
318 let matching_entity = known_entities
320 .iter()
321 .find(|e| e.blades == output_blades)
322 .map(|e| e.name.clone());
323
324 BladeProductResult {
325 output_blades,
326 output_grades,
327 matching_entity,
328 is_zero: false,
329 }
330}
331
332pub fn infer_all_products_blades(
346 entities: &[EntityBladeSet],
347 product_type: ProductType,
348 algebra: &Algebra,
349) -> Vec<(String, String, BladeProductResult)> {
350 let table = ProductTable::new(algebra);
351 let mut results = Vec::new();
352
353 for lhs in entities {
354 for rhs in entities {
355 let result = infer_product_blades(lhs, rhs, product_type, entities, algebra, &table);
356 results.push((lhs.name.clone(), rhs.name.clone(), result));
357 }
358 }
359
360 results
361}
362
363pub fn infer_output_grades(
393 lhs_grades: &[usize],
394 rhs_grades: &[usize],
395 product_type: ProductType,
396 algebra: &Algebra,
397) -> Vec<usize> {
398 let dim = algebra.dim();
399 let mut output_set = BTreeSet::new();
400
401 for &ga in lhs_grades {
402 for &gb in rhs_grades {
403 match product_type {
404 ProductType::Geometric => {
405 for g in geometric_grades(ga, gb, dim) {
406 output_set.insert(g);
407 }
408 }
409 ProductType::Exterior => {
410 if let Some(g) = outer_grade(ga, gb, dim) {
411 output_set.insert(g);
412 }
413 }
414 ProductType::LeftContraction => {
415 if let Some(g) = left_contraction_grade(ga, gb) {
416 output_set.insert(g);
417 }
418 }
419 ProductType::RightContraction => {
420 if gb <= ga {
422 output_set.insert(ga - gb);
423 }
424 }
425 ProductType::Regressive => {
426 let result = ga + gb;
429 if result >= dim {
430 output_set.insert(result - dim);
431 }
432 }
433 ProductType::Scalar => {
434 if ga == gb {
436 output_set.insert(0);
437 }
438 }
439 ProductType::Antigeometric => {
440 for g in geometric_grades(ga, gb, dim) {
443 output_set.insert(g);
444 }
445 }
446 ProductType::Antiscalar => {
447 if ga + gb >= dim && (ga + gb - dim).is_multiple_of(2) {
449 output_set.insert(dim);
451 }
452 }
453 ProductType::BulkContraction | ProductType::WeightContraction => {
454 if ga >= gb {
458 output_set.insert(ga - gb);
459 }
460 }
461 ProductType::BulkExpansion | ProductType::WeightExpansion => {
462 let result = ga + dim - gb;
466 if result <= dim {
467 output_set.insert(result);
468 }
469 }
470 ProductType::Dot => {
471 if ga == gb {
474 output_set.insert(0);
475 }
476 }
477 ProductType::Antidot => {
478 if ga == gb {
482 output_set.insert(0);
483 }
484 }
485 ProductType::Project => {
486 output_set.insert(ga);
492 }
493 ProductType::Antiproject => {
494 if ga >= gb {
500 output_set.insert(ga);
501 }
502 }
503 }
504 }
505 }
506
507 output_set.into_iter().collect()
508}
509
510pub fn infer_output_grades_precise(
528 lhs_grades: &[usize],
529 rhs_grades: &[usize],
530 product_type: ProductType,
531 algebra: &Algebra,
532 table: &ProductTable,
533) -> Vec<usize> {
534 let lhs_blades = blades_of_grades(algebra.dim(), lhs_grades);
535 let rhs_blades = blades_of_grades(algebra.dim(), rhs_grades);
536 let mut output_set = BTreeSet::new();
537
538 for &a in &lhs_blades {
539 for &b in &rhs_blades {
540 let (sign, result_grade) = match product_type {
542 ProductType::Regressive => {
543 let (sign, result) = table.regressive(a, b);
545 (sign, grade(result))
546 }
547 ProductType::Exterior => {
548 let (sign, result) = table.exterior(a, b);
550 (sign, grade(result))
551 }
552 ProductType::Antigeometric | ProductType::Antiscalar => {
553 let (sign, result) = table.antiproduct(a, b);
555 (sign, grade(result))
556 }
557 ProductType::BulkContraction => {
558 let (sign, result) = table.bulk_contraction(a, b);
559 (sign, grade(result))
560 }
561 ProductType::WeightContraction => {
562 let (sign, result) = table.weight_contraction(a, b);
563 (sign, grade(result))
564 }
565 ProductType::BulkExpansion => {
566 let (sign, result) = table.bulk_expansion(a, b);
567 (sign, grade(result))
568 }
569 ProductType::WeightExpansion => {
570 let (sign, result) = table.weight_expansion(a, b);
571 (sign, grade(result))
572 }
573 ProductType::Dot => {
574 let (sign, result) = table.dot(a, b);
575 (sign, grade(result))
576 }
577 ProductType::Antidot => {
578 let (sign, result) = table.antidot(a, b);
579 (sign, grade(result))
580 }
581 ProductType::Project => {
582 let (sign, result) = table.project(a, b);
583 (sign, grade(result))
584 }
585 ProductType::Antiproject => {
586 let (sign, result) = table.antiproject(a, b);
587 (sign, grade(result))
588 }
589 _ => {
590 let (sign, result) = table.geometric(a, b);
592 (sign, grade(result))
593 }
594 };
595
596 if sign == 0 {
597 continue;
598 }
599
600 let ga = grade(a);
601 let gb = grade(b);
602
603 let include = match product_type {
605 ProductType::Geometric => true,
606 ProductType::Exterior => {
607 result_grade == ga + gb
609 }
610 ProductType::LeftContraction => {
611 ga <= gb && result_grade == gb - ga
613 }
614 ProductType::RightContraction => {
615 gb <= ga && result_grade == ga - gb
617 }
618 ProductType::Regressive => {
619 true
621 }
622 ProductType::Scalar => {
623 result_grade == 0
625 }
626 ProductType::Antigeometric => {
627 true
629 }
630 ProductType::Antiscalar => {
631 result_grade == algebra.dim()
633 }
634 ProductType::BulkContraction
635 | ProductType::WeightContraction
636 | ProductType::BulkExpansion
637 | ProductType::WeightExpansion => {
638 true
640 }
641 ProductType::Dot => {
642 ga == gb && result_grade == 0
644 }
645 ProductType::Antidot => {
646 ga == gb && result_grade == 0
648 }
649 ProductType::Project | ProductType::Antiproject => {
650 true
652 }
653 };
654
655 if include {
656 output_set.insert(result_grade);
657 }
658 }
659 }
660
661 output_set.into_iter().collect()
662}
663
664pub fn infer_product(
688 lhs_grades: &[usize],
689 rhs_grades: &[usize],
690 product_type: ProductType,
691 known_entities: &[(Vec<usize>, String)],
692 algebra: &Algebra,
693 table: &ProductTable,
694) -> ProductResult {
695 let raw_output =
696 infer_output_grades_precise(lhs_grades, rhs_grades, product_type, algebra, table);
697
698 if raw_output.is_empty() {
699 return ProductResult {
700 output_grades: vec![],
701 matching_entity: None,
702 is_zero: true,
703 };
704 }
705
706 if let Some((_, name)) = known_entities
709 .iter()
710 .find(|(grades, _)| grades == &raw_output)
711 {
712 return ProductResult {
713 output_grades: raw_output,
714 matching_entity: Some(name.clone()),
715 is_zero: false,
716 };
717 }
718
719 ProductResult {
722 output_grades: raw_output,
723 matching_entity: None,
724 is_zero: false,
725 }
726}
727
728#[derive(Debug, Clone)]
730pub struct ProductTable2D {
731 pub product_type: ProductType,
733 pub entries: Vec<(String, String, ProductResult)>,
735}
736
737pub fn infer_all_products(
749 entities: &[(String, Vec<usize>)],
750 product_type: ProductType,
751 algebra: &Algebra,
752) -> ProductTable2D {
753 let table = ProductTable::new(algebra);
754 let known_entities: Vec<(Vec<usize>, String)> = entities
755 .iter()
756 .map(|(name, grades)| (grades.clone(), name.clone()))
757 .collect();
758
759 let mut entries = Vec::new();
760
761 for (lhs_name, lhs_grades) in entities {
762 for (rhs_name, rhs_grades) in entities {
763 let result = infer_product(
764 lhs_grades,
765 rhs_grades,
766 product_type,
767 &known_entities,
768 algebra,
769 &table,
770 );
771 entries.push((lhs_name.clone(), rhs_name.clone(), result));
772 }
773 }
774
775 ProductTable2D {
776 product_type,
777 entries,
778 }
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784
785 #[test]
786 fn geometric_vector_vector() {
787 let algebra = Algebra::euclidean(3);
788
789 let output = infer_output_grades(&[1], &[1], ProductType::Geometric, &algebra);
791 assert_eq!(output, vec![0, 2]);
792 }
793
794 #[test]
795 fn geometric_vector_vector_precise() {
796 let algebra = Algebra::euclidean(3);
797 let table = ProductTable::new(&algebra);
798
799 let output =
801 infer_output_grades_precise(&[1], &[1], ProductType::Geometric, &algebra, &table);
802 assert_eq!(output, vec![0, 2]);
803 }
804
805 #[test]
806 fn outer_vector_vector() {
807 let algebra = Algebra::euclidean(3);
808
809 let output = infer_output_grades(&[1], &[1], ProductType::Exterior, &algebra);
811 assert_eq!(output, vec![2]);
812 }
813
814 #[test]
815 fn outer_vector_bivector() {
816 let algebra = Algebra::euclidean(3);
817
818 let output = infer_output_grades(&[1], &[2], ProductType::Exterior, &algebra);
820 assert_eq!(output, vec![3]);
821 }
822
823 #[test]
824 fn outer_bivector_bivector() {
825 let algebra = Algebra::euclidean(3);
826
827 let output = infer_output_grades(&[2], &[2], ProductType::Exterior, &algebra);
829 assert!(output.is_empty());
830 }
831
832 #[test]
833 fn left_contraction_vector_bivector() {
834 let algebra = Algebra::euclidean(3);
835
836 let output = infer_output_grades(&[1], &[2], ProductType::LeftContraction, &algebra);
838 assert_eq!(output, vec![1]);
839 }
840
841 #[test]
842 fn left_contraction_bivector_vector() {
843 let algebra = Algebra::euclidean(3);
844
845 let output = infer_output_grades(&[2], &[1], ProductType::LeftContraction, &algebra);
847 assert!(output.is_empty());
848 }
849
850 #[test]
851 fn rotor_geometric_products() {
852 let algebra = Algebra::euclidean(3);
853
854 let output = infer_output_grades(&[0, 2], &[0, 2], ProductType::Geometric, &algebra);
856 assert_eq!(output, vec![0, 2]);
857
858 let output = infer_output_grades(&[0, 2], &[1], ProductType::Geometric, &algebra);
860 assert_eq!(output, vec![1, 3]);
861 }
862
863 #[test]
864 fn infer_product_with_matching() {
865 let algebra = Algebra::euclidean(3);
866 let table = ProductTable::new(&algebra);
867
868 let entities = vec![
869 (vec![0], "Entity_0".to_string()),
870 (vec![1], "Entity_1".to_string()),
871 (vec![2], "Entity_2".to_string()),
872 (vec![0, 2], "Entity_0_2".to_string()),
873 ];
874
875 let result = infer_product(
877 &[1],
878 &[1],
879 ProductType::Geometric,
880 &entities,
881 &algebra,
882 &table,
883 );
884 assert_eq!(result.output_grades, vec![0, 2]);
885 assert_eq!(result.matching_entity, Some("Entity_0_2".to_string()));
886 assert!(!result.is_zero);
887
888 let result = infer_product(
890 &[1],
891 &[1],
892 ProductType::Exterior,
893 &entities,
894 &algebra,
895 &table,
896 );
897 assert_eq!(result.output_grades, vec![2]);
898 assert_eq!(result.matching_entity, Some("Entity_2".to_string()));
899 assert!(!result.is_zero);
900 }
901
902 #[test]
903 fn infer_product_requires_exact_match() {
904 let algebra = Algebra::euclidean(3);
905 let table = ProductTable::new(&algebra);
906
907 let entities = vec![
909 (vec![0], "Entity_0".to_string()),
910 (vec![1], "Entity_1".to_string()),
911 ];
912
913 let result = infer_product(
916 &[1],
917 &[1],
918 ProductType::Geometric,
919 &entities,
920 &algebra,
921 &table,
922 );
923 assert_eq!(result.output_grades, vec![0, 2]);
924 assert!(result.matching_entity.is_none());
925 assert!(!result.is_zero);
926 }
927
928 #[test]
929 fn infer_product_no_matching_entity() {
930 let algebra = Algebra::euclidean(3);
931 let table = ProductTable::new(&algebra);
932
933 let entities = vec![(vec![3], "Entity_3".to_string())];
935
936 let result = infer_product(
938 &[1],
939 &[1],
940 ProductType::Geometric,
941 &entities,
942 &algebra,
943 &table,
944 );
945 assert_eq!(result.output_grades, vec![0, 2]);
946 assert!(result.matching_entity.is_none());
947 assert!(!result.is_zero);
948 }
949
950 #[test]
951 fn infer_product_zero() {
952 let algebra = Algebra::euclidean(3);
953 let table = ProductTable::new(&algebra);
954
955 let entities = vec![(vec![2], "Entity_2".to_string())];
956
957 let result = infer_product(
959 &[2],
960 &[2],
961 ProductType::Exterior,
962 &entities,
963 &algebra,
964 &table,
965 );
966 assert!(result.output_grades.is_empty());
967 assert!(result.matching_entity.is_none());
968 assert!(result.is_zero);
969 }
970
971 #[test]
972 fn infer_all_products_basic() {
973 let algebra = Algebra::euclidean(3);
974
975 let entities = vec![
977 ("Entity_0".to_string(), vec![0]),
978 ("Entity_1".to_string(), vec![1]),
979 ("Entity_0_2".to_string(), vec![0, 2]),
980 ];
981
982 let table = infer_all_products(&entities, ProductType::Geometric, &algebra);
983 assert_eq!(table.product_type, ProductType::Geometric);
984 assert_eq!(table.entries.len(), 9); let v_times_v = table
988 .entries
989 .iter()
990 .find(|(lhs, rhs, _)| lhs == "Entity_1" && rhs == "Entity_1");
991 assert!(v_times_v.is_some());
992 let (_, _, result) = v_times_v.unwrap();
993 assert_eq!(result.output_grades, vec![0, 2]);
994 assert_eq!(result.matching_entity, Some("Entity_0_2".to_string()));
995 }
996
997 #[test]
998 fn pga_null_basis() {
999 let algebra = Algebra::pga(3); let table = ProductTable::new(&algebra);
1004
1005 let output =
1007 infer_output_grades_precise(&[1], &[1], ProductType::Geometric, &algebra, &table);
1008 assert!(output.contains(&0));
1009 assert!(output.contains(&2));
1010 }
1011
1012 #[test]
1013 fn product_with_constraint_application() {
1014 let algebra = Algebra::euclidean(3);
1015 let table = ProductTable::new(&algebra);
1016
1017 let entities = vec![
1019 (vec![0], "Entity_0".to_string()),
1020 (vec![1], "Entity_1".to_string()),
1021 (vec![2], "Entity_2".to_string()),
1022 (vec![3], "Entity_3".to_string()),
1023 (vec![0, 2], "Entity_0_2".to_string()),
1024 (vec![1, 3], "Entity_1_3".to_string()),
1025 ];
1026
1027 let result = infer_product(
1029 &[0, 2],
1030 &[1],
1031 ProductType::Geometric,
1032 &entities,
1033 &algebra,
1034 &table,
1035 );
1036 assert_eq!(result.output_grades, vec![1, 3]);
1037 assert_eq!(result.matching_entity, Some("Entity_1_3".to_string()));
1038
1039 let result = infer_product(
1041 &[1, 3],
1042 &[1],
1043 ProductType::Geometric,
1044 &entities,
1045 &algebra,
1046 &table,
1047 );
1048 assert_eq!(result.output_grades, vec![0, 2]);
1049 assert_eq!(result.matching_entity, Some("Entity_0_2".to_string()));
1050
1051 let result = infer_product(
1053 &[0, 2],
1054 &[0, 2],
1055 ProductType::Geometric,
1056 &entities,
1057 &algebra,
1058 &table,
1059 );
1060 assert_eq!(result.output_grades, vec![0, 2]);
1061 assert_eq!(result.matching_entity, Some("Entity_0_2".to_string()));
1062 }
1063
1064 #[test]
1069 fn report_missing_product_matches() {
1070 use crate::spec::parse_spec;
1071
1072 let algebras = [
1073 ("euclidean2", include_str!("../../algebras/euclidean2.toml")),
1074 ("euclidean3", include_str!("../../algebras/euclidean3.toml")),
1075 (
1076 "projective2",
1077 include_str!("../../algebras/projective2.toml"),
1078 ),
1079 (
1080 "projective3",
1081 include_str!("../../algebras/projective3.toml"),
1082 ),
1083 ("conformal3", include_str!("../../algebras/conformal3.toml")),
1084 ("quaternion", include_str!("../../algebras/quaternion.toml")),
1085 ("dualquat", include_str!("../../algebras/dualquat.toml")),
1086 ("complex", include_str!("../../algebras/complex.toml")),
1087 ("dual", include_str!("../../algebras/dual.toml")),
1088 ("hyperbolic", include_str!("../../algebras/hyperbolic.toml")),
1089 ("minkowski2", include_str!("../../algebras/minkowski2.toml")),
1090 ("minkowski3", include_str!("../../algebras/minkowski3.toml")),
1091 ("elliptic2", include_str!("../../algebras/elliptic2.toml")),
1092 (
1093 "hyperbolic2",
1094 include_str!("../../algebras/hyperbolic2.toml"),
1095 ),
1096 ];
1097
1098 let product_types = [
1099 ProductType::Geometric,
1100 ProductType::Exterior,
1101 ProductType::LeftContraction,
1102 ProductType::Regressive,
1103 ];
1104
1105 let mut total_missing = 0;
1106
1107 for (name, toml) in &algebras {
1108 let spec = parse_spec(toml).unwrap();
1109 let algebra = Algebra::new(spec.signature.p, spec.signature.q, spec.signature.r);
1110
1111 let entities: Vec<(String, Vec<usize>)> = spec
1113 .types
1114 .iter()
1115 .filter(|t| t.alias_of.is_none() && !t.is_sparse)
1116 .map(|t| (t.name.clone(), t.grades.clone()))
1117 .collect();
1118
1119 let mut algebra_missing = 0;
1120
1121 for product_type in &product_types {
1122 let table = infer_all_products(&entities, *product_type, &algebra);
1123
1124 for (lhs, rhs, result) in &table.entries {
1125 if !result.is_zero && result.matching_entity.is_none() {
1126 algebra_missing += 1;
1127 eprintln!(
1128 " {}: {} {:?} {} -> grades {:?} (no match)",
1129 name,
1130 lhs,
1131 product_type.toml_name(),
1132 rhs,
1133 result.output_grades
1134 );
1135 }
1136 }
1137 }
1138
1139 if algebra_missing > 0 {
1140 eprintln!("{}: {} missing product matches", name, algebra_missing);
1141 }
1142 total_missing += algebra_missing;
1143 }
1144
1145 eprintln!("\nTotal missing product matches: {}", total_missing);
1146 }
1150
1151 #[test]
1152 fn entity_blade_set_from_grades() {
1153 let entity = EntityBladeSet::from_grades("Vector".to_string(), vec![1], 3);
1154
1155 assert_eq!(entity.name, "Vector");
1157 assert_eq!(entity.grades, vec![1]);
1158 assert!(!entity.is_sparse);
1159 assert!(entity.blades.contains(&1)); assert!(entity.blades.contains(&2)); assert!(entity.blades.contains(&4)); assert_eq!(entity.blades.len(), 3);
1163 }
1164
1165 #[test]
1166 fn entity_blade_set_sparse() {
1167 let entity = EntityBladeSet::new("Partial".to_string(), vec![1, 2]); assert_eq!(entity.name, "Partial");
1171 assert_eq!(entity.grades, vec![1]);
1172 assert!(entity.is_sparse, "Entity should be sparse (missing e3)");
1173 assert!(entity.blades.contains(&1)); assert!(entity.blades.contains(&2)); assert!(!entity.blades.contains(&4)); }
1177
1178 #[test]
1179 fn infer_output_blades_geometric() {
1180 let algebra = Algebra::euclidean(3);
1181 let table = ProductTable::new(&algebra);
1182
1183 let lhs_blades: BTreeSet<usize> = vec![1, 2, 4].into_iter().collect(); let rhs_blades: BTreeSet<usize> = vec![1, 2, 4].into_iter().collect();
1186
1187 let output = infer_output_blades(
1188 &lhs_blades,
1189 &rhs_blades,
1190 ProductType::Geometric,
1191 &algebra,
1192 &table,
1193 );
1194
1195 assert!(output.contains(&0)); assert!(output.contains(&3)); assert!(output.contains(&5)); assert!(output.contains(&6)); }
1201
1202 #[test]
1203 fn infer_output_blades_sparse_geometric() {
1204 let algebra = Algebra::euclidean(3);
1205 let table = ProductTable::new(&algebra);
1206
1207 let lhs_blades: BTreeSet<usize> = vec![1, 2].into_iter().collect(); let rhs_blades: BTreeSet<usize> = vec![1, 2, 4].into_iter().collect(); let output = infer_output_blades(
1212 &lhs_blades,
1213 &rhs_blades,
1214 ProductType::Geometric,
1215 &algebra,
1216 &table,
1217 );
1218
1219 assert!(output.contains(&0)); assert!(output.contains(&3)); assert!(output.contains(&5)); assert!(output.contains(&6)); assert!(!output.contains(&4)); }
1230
1231 #[test]
1232 fn infer_product_blades_with_matching() {
1233 let algebra = Algebra::euclidean(3);
1234 let table = ProductTable::new(&algebra);
1235
1236 let scalar = EntityBladeSet::from_grades("Scalar".to_string(), vec![0], 3);
1237 let vector = EntityBladeSet::from_grades("Vector".to_string(), vec![1], 3);
1238 let bivector = EntityBladeSet::from_grades("Bivector".to_string(), vec![2], 3);
1239 let rotor = EntityBladeSet::from_grades("Rotor".to_string(), vec![0, 2], 3);
1240
1241 let entities = vec![
1242 scalar.clone(),
1243 vector.clone(),
1244 bivector.clone(),
1245 rotor.clone(),
1246 ];
1247
1248 let result = infer_product_blades(
1250 &vector,
1251 &vector,
1252 ProductType::Geometric,
1253 &entities,
1254 &algebra,
1255 &table,
1256 );
1257
1258 assert_eq!(result.output_grades, vec![0, 2]);
1259 assert_eq!(result.matching_entity, Some("Rotor".to_string()));
1260 assert!(!result.is_zero);
1261 }
1262
1263 #[test]
1264 fn infer_product_blades_sparse_no_match() {
1265 let algebra = Algebra::euclidean(3);
1266 let table = ProductTable::new(&algebra);
1267
1268 let partial_vec = EntityBladeSet::new("PartialVec".to_string(), vec![1, 2]);
1270 let full_vec = EntityBladeSet::from_grades("Vector".to_string(), vec![1], 3);
1272 let rotor = EntityBladeSet::from_grades("Rotor".to_string(), vec![0, 2], 3);
1274
1275 let entities = vec![partial_vec.clone(), full_vec.clone(), rotor.clone()];
1276
1277 let result = infer_product_blades(
1280 &partial_vec,
1281 &full_vec,
1282 ProductType::Geometric,
1283 &entities,
1284 &algebra,
1285 &table,
1286 );
1287
1288 assert_eq!(result.output_grades, vec![0, 2]);
1292 assert_eq!(result.matching_entity, Some("Rotor".to_string()));
1293 }
1294
1295 #[test]
1296 fn infer_all_products_blades_basic() {
1297 let algebra = Algebra::euclidean(3);
1298
1299 let scalar = EntityBladeSet::from_grades("Scalar".to_string(), vec![0], 3);
1300 let vector = EntityBladeSet::from_grades("Vector".to_string(), vec![1], 3);
1301 let rotor = EntityBladeSet::from_grades("Rotor".to_string(), vec![0, 2], 3);
1302
1303 let entities = vec![scalar, vector, rotor];
1304
1305 let results = infer_all_products_blades(&entities, ProductType::Geometric, &algebra);
1306
1307 assert_eq!(results.len(), 9); let vv = results
1311 .iter()
1312 .find(|(l, r, _)| l == "Vector" && r == "Vector");
1313 assert!(vv.is_some());
1314 let (_, _, result) = vv.unwrap();
1315 assert_eq!(result.matching_entity, Some("Rotor".to_string()));
1316 }
1317}