Skip to main content

clifford_codegen/discovery/
products.rs

1//! Product output inference for discovered entities.
2//!
3//! This module infers the output type for products between discovered entities.
4//! Given two entities and a product type, it computes which grades appear in
5//! the output and matches them to known entities.
6//!
7//! Products are only generated when the output grades exactly match a known
8//! entity type. This prevents generating incorrect code that loses grade
9//! components.
10
11use 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/// Represents a product type for inference.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum ProductType {
20    /// Geometric product: `a * b`
21    Geometric,
22    /// Exterior (wedge) product: `a ∧ b`
23    Exterior,
24    /// Left contraction: `a ⌋ b`
25    LeftContraction,
26    /// Right contraction: `a ⌊ b`
27    RightContraction,
28    /// Regressive (meet) product: `a ∨ b`
29    Regressive,
30    /// Scalar product: grade-0 projection of geometric
31    Scalar,
32    /// Antigeometric product: `a ⊟ b`
33    Antigeometric,
34    /// Antiscalar product: grade-n projection of antigeometric
35    Antiscalar,
36    /// Bulk contraction: `a ∨ b★` (antiwedge with bulk dual)
37    BulkContraction,
38    /// Weight contraction: `a ∨ b☆` (antiwedge with weight dual)
39    WeightContraction,
40    /// Bulk expansion: `a ∧ b★` (wedge with bulk dual)
41    BulkExpansion,
42    /// Weight expansion: `a ∧ b☆` (wedge with weight dual)
43    WeightExpansion,
44    /// Dot product: `a • b` (metric inner, same-grade only, returns scalar)
45    Dot,
46    /// Antidot product: `a ⊚ b` (metric antiproduct inner, same-antigrade only, returns scalar)
47    Antidot,
48    /// Projection: `b ∨ (a ∧ b☆)` (target antiwedge with wedge of self and weight dual of target)
49    Project,
50    /// Antiprojection: `b ∧ (a ∨ b☆)` (target wedge with antiwedge of self and weight dual of target)
51    Antiproject,
52}
53
54impl ProductType {
55    /// Returns all standard product types.
56    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    /// Returns the name for TOML output.
78    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/// Result of product inference between two entities.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ProductResult {
103    /// Grades present in the output after applying constraints.
104    /// This is the constrained/simplified output that satisfies geometric constraints.
105    pub output_grades: Vec<usize>,
106    /// Name of matching entity, if one exists.
107    pub matching_entity: Option<String>,
108    /// Whether the output is always zero (after constraint application).
109    pub is_zero: bool,
110}
111
112/// Entity representation with exact blade set for blade-level inference.
113///
114/// Used for sparse types that don't span all blades of their grades.
115#[derive(Debug, Clone)]
116pub struct EntityBladeSet {
117    /// Entity name.
118    pub name: String,
119    /// Exact blade indices this entity contains.
120    pub blades: BTreeSet<usize>,
121    /// Grades spanned by this entity.
122    pub grades: Vec<usize>,
123    /// Whether this is a sparse type.
124    pub is_sparse: bool,
125}
126
127impl EntityBladeSet {
128    /// Creates a new EntityBladeSet from a name and blade indices.
129    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    /// Creates a non-sparse EntityBladeSet from grades.
147    ///
148    /// This creates an entity that spans all blades of the specified grades.
149    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    /// Checks if blades span all blades of the given grades.
160    fn spans_all_grades(blades: &BTreeSet<usize>, grades: &[usize]) -> bool {
161        // Count expected blades for each grade
162        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/// Result of blade-level product inference.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct BladeProductResult {
185    /// Blade indices present in the output.
186    pub output_blades: BTreeSet<usize>,
187    /// Grades present in the output.
188    pub output_grades: Vec<usize>,
189    /// Name of matching entity, if one exists.
190    pub matching_entity: Option<String>,
191    /// Whether the output is always zero.
192    pub is_zero: bool,
193}
194
195/// Infers the output blades for a product between two blade sets.
196///
197/// This function computes the exact blades that appear in the product output,
198/// which is necessary for sparse types that don't span all blades of their grades.
199///
200/// # Arguments
201///
202/// * `lhs_blades` - Blade indices in the left operand
203/// * `rhs_blades` - Blade indices in the right operand
204/// * `product_type` - The type of product
205/// * `algebra` - The algebra
206/// * `table` - Precomputed product table
207///
208/// # Returns
209///
210/// The set of blade indices that appear in non-zero output.
211pub 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            // Get product result using appropriate method
223            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            // Check if this product should be included based on product type
247            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
274/// Infers the product output at blade level and matches to known entities.
275///
276/// This function handles sparse types correctly by computing products at the
277/// blade level instead of the grade level.
278///
279/// # Arguments
280///
281/// * `lhs` - Left operand entity with exact blades
282/// * `rhs` - Right operand entity with exact blades
283/// * `product_type` - The type of product
284/// * `known_entities` - List of entities with their blade sets
285/// * `algebra` - The algebra
286/// * `table` - Precomputed product table
287///
288/// # Returns
289///
290/// The inferred blade product result.
291pub 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    // Compute output grades
311    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    // Match to known entities by exact blade set
319    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
332/// Infers all products between entities using blade-level inference.
333///
334/// This version handles sparse types correctly by computing at the blade level.
335///
336/// # Arguments
337///
338/// * `entities` - List of entities with their blade sets
339/// * `product_type` - The type of product
340/// * `algebra` - The algebra
341///
342/// # Returns
343///
344/// A list of (lhs_name, rhs_name, result) tuples.
345pub 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
363/// Infers the output grades for a product between two grade sets.
364///
365/// # Arguments
366///
367/// * `lhs_grades` - Grades in the left operand
368/// * `rhs_grades` - Grades in the right operand
369/// * `product_type` - The type of product
370/// * `algebra` - The algebra (for dimension and actual computation)
371///
372/// # Returns
373///
374/// The set of grades that can appear in the output.
375///
376/// # Example
377///
378/// ```
379/// use clifford_codegen::discovery::products::{ProductType, infer_output_grades};
380/// use clifford_codegen::algebra::Algebra;
381///
382/// let algebra = Algebra::euclidean(3);
383///
384/// // Vector * Vector produces scalar + bivector (grades 0, 2)
385/// let output = infer_output_grades(&[1], &[1], ProductType::Geometric, &algebra);
386/// assert_eq!(output, vec![0, 2]);
387///
388/// // Vector ∧ Vector produces bivector (grade 2)
389/// let output = infer_output_grades(&[1], &[1], ProductType::Exterior, &algebra);
390/// assert_eq!(output, vec![2]);
391/// ```
392pub 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                    // Right contraction: grade(a) - grade(b) when gb <= ga
421                    if gb <= ga {
422                        output_set.insert(ga - gb);
423                    }
424                }
425                ProductType::Regressive => {
426                    // Regressive product: (a* ∧ b*)* where * is dual
427                    // Result grade = ga + gb - dim (when >= 0)
428                    let result = ga + gb;
429                    if result >= dim {
430                        output_set.insert(result - dim);
431                    }
432                }
433                ProductType::Scalar => {
434                    // Scalar product: only grade 0 from geometric
435                    if ga == gb {
436                        output_set.insert(0);
437                    }
438                }
439                ProductType::Antigeometric => {
440                    // Antigeometric: dual(dual(a) * dual(b))
441                    // Same grade structure as geometric, just different metric
442                    for g in geometric_grades(ga, gb, dim) {
443                        output_set.insert(g);
444                    }
445                }
446                ProductType::Antiscalar => {
447                    // Antiscalar: only grade dim from antigeometric
448                    if ga + gb >= dim && (ga + gb - dim).is_multiple_of(2) {
449                        // Can contribute to pseudoscalar
450                        output_set.insert(dim);
451                    }
452                }
453                ProductType::BulkContraction | ProductType::WeightContraction => {
454                    // Contraction: a ∨ dual(b)
455                    // dual(b) has antigrade = dim - gb
456                    // antiwedge: ga + (dim - gb) - dim = ga - gb (when ga >= gb)
457                    if ga >= gb {
458                        output_set.insert(ga - gb);
459                    }
460                }
461                ProductType::BulkExpansion | ProductType::WeightExpansion => {
462                    // Expansion: a ∧ dual(b)
463                    // dual(b) has antigrade = dim - gb
464                    // wedge: ga + (dim - gb) (when <= dim)
465                    let result = ga + dim - gb;
466                    if result <= dim {
467                        output_set.insert(result);
468                    }
469                }
470                ProductType::Dot => {
471                    // Dot product: only same-grade elements produce non-zero
472                    // Returns scalar (grade 0)
473                    if ga == gb {
474                        output_set.insert(0);
475                    }
476                }
477                ProductType::Antidot => {
478                    // Antidot product: only same-antigrade elements produce non-zero
479                    // Since antigrade = dim - grade, same-antigrade means same-grade
480                    // Returns scalar (grade 0)
481                    if ga == gb {
482                        output_set.insert(0);
483                    }
484                }
485                ProductType::Project => {
486                    // Project: b ∨ (a ∧ b☆)
487                    // b☆ has grade = dim - gb (weight dual)
488                    // a ∧ b☆ has grade = ga + (dim - gb) when <= dim
489                    // b ∨ (a ∧ b☆) has grade = gb + (ga + dim - gb) - dim = ga
490                    // So projection preserves the grade of a
491                    output_set.insert(ga);
492                }
493                ProductType::Antiproject => {
494                    // Antiproject: b ∧ (a ∨ b☆)
495                    // b☆ has grade = dim - gb
496                    // a ∨ b☆ has grade = ga + (dim - gb) - dim = ga - gb when ga >= gb
497                    // b ∧ (a ∨ b☆) has grade = gb + (ga - gb) = ga when ga >= gb
498                    // So antiprojection also produces grade ga when defined
499                    if ga >= gb {
500                        output_set.insert(ga);
501                    }
502                }
503            }
504        }
505    }
506
507    output_set.into_iter().collect()
508}
509
510/// Infers the output grades using actual product computation.
511///
512/// This is more precise than `infer_output_grades` because it considers
513/// the metric signature. Some products may be zero due to the metric
514/// even though the grades would theoretically be present.
515///
516/// # Arguments
517///
518/// * `lhs_grades` - Grades in the left operand
519/// * `rhs_grades` - Grades in the right operand
520/// * `product_type` - The type of product
521/// * `algebra` - The algebra (for dimension and product computation)
522/// * `table` - Precomputed product table
523///
524/// # Returns
525///
526/// The set of grades that actually appear in non-zero output.
527pub 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            // For products with specialized computation, use the appropriate method
541            let (sign, result_grade) = match product_type {
542                ProductType::Regressive => {
543                    // Regressive product uses complement-based formula
544                    let (sign, result) = table.regressive(a, b);
545                    (sign, grade(result))
546                }
547                ProductType::Exterior => {
548                    // Exterior product has its own method
549                    let (sign, result) = table.exterior(a, b);
550                    (sign, grade(result))
551                }
552                ProductType::Antigeometric | ProductType::Antiscalar => {
553                    // Antiproducts use the anti-metric
554                    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                    // All other products derive from the geometric product
591                    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            // Check if this product should be included based on product type
604            let include = match product_type {
605                ProductType::Geometric => true,
606                ProductType::Exterior => {
607                    // Already filtered by exterior method, but verify grade
608                    result_grade == ga + gb
609                }
610                ProductType::LeftContraction => {
611                    // Left contraction: only grade gb - ga terms (when ga <= gb)
612                    ga <= gb && result_grade == gb - ga
613                }
614                ProductType::RightContraction => {
615                    // Right contraction: only grade ga - gb terms (when gb <= ga)
616                    gb <= ga && result_grade == ga - gb
617                }
618                ProductType::Regressive => {
619                    // Already computed correctly by regressive method
620                    true
621                }
622                ProductType::Scalar => {
623                    // Scalar product: only grade 0 terms
624                    result_grade == 0
625                }
626                ProductType::Antigeometric => {
627                    // Already computed by antiproduct, include all
628                    true
629                }
630                ProductType::Antiscalar => {
631                    // Antiscalar: only grade dim terms
632                    result_grade == algebra.dim()
633                }
634                ProductType::BulkContraction
635                | ProductType::WeightContraction
636                | ProductType::BulkExpansion
637                | ProductType::WeightExpansion => {
638                    // Already computed correctly by specialized table methods
639                    true
640                }
641                ProductType::Dot => {
642                    // Dot product: only same-grade, only scalar output
643                    ga == gb && result_grade == 0
644                }
645                ProductType::Antidot => {
646                    // Antidot product: only same-antigrade (same-grade), only scalar output
647                    ga == gb && result_grade == 0
648                }
649                ProductType::Project | ProductType::Antiproject => {
650                    // Already computed correctly by specialized table methods
651                    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
664/// Infers the product output and matches to known entities.
665///
666/// This function:
667/// 1. Computes raw output grades from the product
668/// 2. Matches directly to known entities (exact match only)
669///
670/// Products are only generated when the output grades exactly match a known
671/// entity. Subset matching would generate incorrect code that loses grade
672/// components (e.g., returning Quadvector for an output that should be
673/// Bivector + Quadvector).
674///
675/// # Arguments
676///
677/// * `lhs_grades` - Grades in the left operand
678/// * `rhs_grades` - Grades in the right operand
679/// * `product_type` - The type of product
680/// * `known_entities` - Map from grade set to entity name
681/// * `algebra` - The algebra
682/// * `table` - Precomputed product table
683///
684/// # Returns
685///
686/// The inferred product result.
687pub 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    // Only match if output grades exactly match a known entity
707    // Subset matching would generate incorrect code that loses grade components
708    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    // No exact match - return raw output without entity match
720    // This product won't be generated in code
721    ProductResult {
722        output_grades: raw_output,
723        matching_entity: None,
724        is_zero: false,
725    }
726}
727
728/// Represents a complete product table between entities.
729#[derive(Debug, Clone)]
730pub struct ProductTable2D {
731    /// Product type.
732    pub product_type: ProductType,
733    /// Entries as (lhs_name, rhs_name, result).
734    pub entries: Vec<(String, String, ProductResult)>,
735}
736
737/// Infers all products between a set of entities.
738///
739/// # Arguments
740///
741/// * `entities` - List of (name, grades) pairs
742/// * `product_type` - The type of product
743/// * `algebra` - The algebra
744///
745/// # Returns
746///
747/// A complete product table.
748pub 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        // Vector * Vector = Scalar + Bivector
790        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        // Vector * Vector = Scalar + Bivector (same result with precise computation)
800        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        // Vector ∧ Vector = Bivector
810        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        // Vector ∧ Bivector = Trivector
819        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        // Bivector ∧ Bivector = 0 in 3D (grade 4 > dim 3)
828        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        // Vector ⌋ Bivector = Vector
837        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        // Bivector ⌋ Vector = 0 (grade 2 > grade 1)
846        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        // Even * Even = Even (grades 0, 2)
855        let output = infer_output_grades(&[0, 2], &[0, 2], ProductType::Geometric, &algebra);
856        assert_eq!(output, vec![0, 2]);
857
858        // Even * Vector = Odd (grades 1, 3)
859        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        // Vector * Vector should match Entity_0_2 (grades 0, 2)
876        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        // Vector ∧ Vector should match Entity_2 (grade 2)
889        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        // Only define Entity_0 and Entity_1 (no Entity_0_2)
908        let entities = vec![
909            (vec![0], "Entity_0".to_string()),
910            (vec![1], "Entity_1".to_string()),
911        ];
912
913        // Vector * Vector produces raw [0, 2], but Entity_0_2 doesn't exist
914        // No exact match, so matching_entity is None (product won't be generated)
915        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        // Define only Entity_3 (trivector) - no subset of [0, 2]
934        let entities = vec![(vec![3], "Entity_3".to_string())];
935
936        // Vector * Vector produces raw [0, 2], no subset matches
937        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        // Bivector ∧ Bivector = 0 in 3D
958        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        // Include Entity_0_2 so Vector * Vector can match exactly
976        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); // 3x3 = 9 combinations
985
986        // Find Entity_1 * Entity_1 entry
987        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); // 4D with degenerate basis
1000
1001        // In PGA, the null basis e4 squares to 0
1002        // This affects products involving grade 4 or higher
1003        let table = ProductTable::new(&algebra);
1004
1005        // Single vector still produces scalar + bivector
1006        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        // Define entities including Even (rotor) and Odd
1018        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        // Even * Vector = Odd [1, 3]
1028        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        // Odd * Vector = Even [0, 2]
1040        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        // Even * Even = Even
1052        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    /// Diagnostic test that reports missing product matches for all algebras.
1065    ///
1066    /// This test doesn't fail - it just prints which products are inferred but
1067    /// don't have matching entities in each algebra.
1068    #[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            // Build entity list (exclude sparse and alias types)
1112            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        // This test is informational - it always passes
1147        // If you want to enforce complete coverage, change this to:
1148        // assert_eq!(total_missing, 0, "Some products don't have matching entities");
1149    }
1150
1151    #[test]
1152    fn entity_blade_set_from_grades() {
1153        let entity = EntityBladeSet::from_grades("Vector".to_string(), vec![1], 3);
1154
1155        // Grade 1 in 3D has blades e1=1, e2=2, e3=4
1156        assert_eq!(entity.name, "Vector");
1157        assert_eq!(entity.grades, vec![1]);
1158        assert!(!entity.is_sparse);
1159        assert!(entity.blades.contains(&1)); // e1
1160        assert!(entity.blades.contains(&2)); // e2
1161        assert!(entity.blades.contains(&4)); // e3
1162        assert_eq!(entity.blades.len(), 3);
1163    }
1164
1165    #[test]
1166    fn entity_blade_set_sparse() {
1167        // Create a sparse entity with only 2 of 3 grade-1 blades
1168        let entity = EntityBladeSet::new("Partial".to_string(), vec![1, 2]); // e1, e2 only
1169
1170        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)); // e1
1174        assert!(entity.blades.contains(&2)); // e2
1175        assert!(!entity.blades.contains(&4)); // e3 not present
1176    }
1177
1178    #[test]
1179    fn infer_output_blades_geometric() {
1180        let algebra = Algebra::euclidean(3);
1181        let table = ProductTable::new(&algebra);
1182
1183        // Vector (grade 1) * Vector (grade 1) = Scalar + Bivector
1184        let lhs_blades: BTreeSet<usize> = vec![1, 2, 4].into_iter().collect(); // e1, e2, e3
1185        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        // Should contain scalar (0) and bivectors (3=e12, 5=e13, 6=e23)
1196        assert!(output.contains(&0)); // scalar
1197        assert!(output.contains(&3)); // e12
1198        assert!(output.contains(&5)); // e13
1199        assert!(output.contains(&6)); // e23
1200    }
1201
1202    #[test]
1203    fn infer_output_blades_sparse_geometric() {
1204        let algebra = Algebra::euclidean(3);
1205        let table = ProductTable::new(&algebra);
1206
1207        // Partial vector (just e1, e2) * full vector
1208        let lhs_blades: BTreeSet<usize> = vec![1, 2].into_iter().collect(); // e1, e2 only
1209        let rhs_blades: BTreeSet<usize> = vec![1, 2, 4].into_iter().collect(); // e1, e2, e3
1210
1211        let output = infer_output_blades(
1212            &lhs_blades,
1213            &rhs_blades,
1214            ProductType::Geometric,
1215            &algebra,
1216            &table,
1217        );
1218
1219        // e1*e1 = 1 (scalar), e1*e2 = e12, e1*e3 = e13
1220        // e2*e1 = -e12, e2*e2 = 1, e2*e3 = e23
1221        assert!(output.contains(&0)); // scalar from e1*e1, e2*e2
1222        assert!(output.contains(&3)); // e12 from e1*e2
1223        assert!(output.contains(&5)); // e13 from e1*e3
1224        assert!(output.contains(&6)); // e23 from e2*e3
1225
1226        // e3*anything is not in output since e3 not in lhs
1227        // e3 (index 4) should not appear in output
1228        assert!(!output.contains(&4)); // e3 not in output
1229    }
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        // Vector * Vector should match Rotor
1249        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        // Create a partial vector (sparse) - just e1 and e2
1269        let partial_vec = EntityBladeSet::new("PartialVec".to_string(), vec![1, 2]);
1270        // Create a full vector
1271        let full_vec = EntityBladeSet::from_grades("Vector".to_string(), vec![1], 3);
1272        // Create full types
1273        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        // PartialVec * Vector produces a subset of blades
1278        // The output won't match Rotor exactly because it's missing some bivector components
1279        let result = infer_product_blades(
1280            &partial_vec,
1281            &full_vec,
1282            ProductType::Geometric,
1283            &entities,
1284            &algebra,
1285            &table,
1286        );
1287
1288        // Output should be scalar + 3 bivectors (e12, e13, e23)
1289        // But e13 comes from e1*e3, e23 comes from e2*e3
1290        // This matches the full Rotor blades, so it should match
1291        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); // 3x3 = 9 combinations
1308
1309        // Find Vector * Vector
1310        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}