1use std::collections::HashMap;
18
19use symbolica::atom::Atom;
20
21use crate::algebra::{
22 Algebra, Blade, ProductTable, antireverse_sign, clifford_conjugate_sign, grade,
23 grade_involution_sign, reverse_sign,
24};
25use crate::spec::{InvolutionKind, TypeSpec};
26
27#[derive(Debug, Clone)]
29pub struct DerivedConstraint {
30 pub name: String,
32
33 pub zero_expressions: Vec<Atom>,
38
39 pub constraint_grades: Vec<usize>,
41}
42
43pub struct ConstraintDeriver<'a> {
45 algebra: &'a Algebra,
47 table: ProductTable,
49 involution: InvolutionKind,
51}
52
53impl<'a> ConstraintDeriver<'a> {
54 pub fn new(algebra: &'a Algebra, involution: InvolutionKind) -> Self {
56 let table = ProductTable::new(algebra);
57 Self {
58 algebra,
59 table,
60 involution,
61 }
62 }
63
64 pub fn derive_geometric_constraint(
75 &self,
76 ty: &TypeSpec,
77 field_prefix: &str,
78 ) -> Option<DerivedConstraint> {
79 let dim = self.algebra.dim();
80
81 let symbols = self.create_symbols(ty, field_prefix);
83
84 let mut zero_expressions = Vec::new();
86 let mut constraint_grades = Vec::new();
87
88 for output_grade in 1..=dim {
90 let expr = self.compute_product_involution_at_grade(ty, output_grade, &symbols);
91
92 if !self.is_zero(&expr) {
94 zero_expressions.push(expr);
95 constraint_grades.push(output_grade);
96 }
97 }
98
99 if zero_expressions.is_empty() {
100 None
101 } else {
102 Some(DerivedConstraint {
103 name: "geometric".to_string(),
104 zero_expressions,
105 constraint_grades,
106 })
107 }
108 }
109
110 fn create_symbols(&self, ty: &TypeSpec, prefix: &str) -> HashMap<usize, Atom> {
112 use std::borrow::Cow;
113 use symbolica::atom::DefaultNamespace;
114 use symbolica::parser::ParseSettings;
115
116 ty.fields
117 .iter()
118 .map(|f| {
119 let symbol_name = format!("{}_{}", prefix, f.name);
120 let input = DefaultNamespace {
121 namespace: Cow::Borrowed(env!("CARGO_CRATE_NAME")),
122 data: symbol_name.as_str(),
123 file: Cow::Borrowed(file!()),
124 line: line!() as usize,
125 };
126 let atom = Atom::parse(input, ParseSettings::symbolica()).unwrap();
127 (f.blade_index, atom)
128 })
129 .collect()
130 }
131
132 fn compute_product_involution_at_grade(
134 &self,
135 ty: &TypeSpec,
136 output_grade: usize,
137 symbols: &HashMap<usize, Atom>,
138 ) -> Atom {
139 let mut terms: Vec<Atom> = Vec::new();
140
141 for field_a in &ty.fields {
144 for field_b in &ty.fields {
145 let blade_a = field_a.blade_index;
146 let blade_b = field_b.blade_index;
147 let grade_b = Blade::from_index(blade_b).grade();
148
149 let (product_sign, result_blade) = self.table.geometric(blade_a, blade_b);
151
152 let result_grade = grade(result_blade);
154 if result_grade != output_grade || product_sign == 0 {
155 continue;
156 }
157
158 let inv_sign = match self.involution {
160 InvolutionKind::Reverse => reverse_sign(grade_b),
161 InvolutionKind::GradeInvolution => grade_involution_sign(grade_b),
162 InvolutionKind::CliffordConjugate => clifford_conjugate_sign(grade_b),
163 };
164 let total_sign = product_sign * inv_sign;
165
166 let sym_a = symbols.get(&blade_a).unwrap();
168 let sym_b = symbols.get(&blade_b).unwrap();
169
170 let product = sym_a * sym_b;
172 let term = if total_sign > 0 { product } else { -product };
173 terms.push(term);
174 }
175 }
176
177 if terms.is_empty() {
178 Atom::num(0)
179 } else {
180 terms.sort_by_cached_key(|t| t.to_string());
182 terms.into_iter().reduce(|acc, t| acc + t).unwrap()
183 }
184 }
185
186 fn is_zero(&self, expr: &Atom) -> bool {
188 *expr == Atom::num(0)
190 }
191
192 pub fn derive_antiproduct_constraint(
205 &self,
206 ty: &TypeSpec,
207 field_prefix: &str,
208 ) -> Option<DerivedConstraint> {
209 let dim = self.algebra.dim();
210
211 let symbols = self.create_symbols(ty, field_prefix);
213
214 let mut zero_expressions = Vec::new();
216 let mut constraint_grades = Vec::new();
217
218 for output_grade in 0..dim {
220 let expr = self.compute_antiproduct_antireverse_at_grade(ty, output_grade, &symbols);
221
222 if !self.is_zero(&expr) {
224 zero_expressions.push(expr);
225 constraint_grades.push(output_grade);
226 }
227 }
228
229 if zero_expressions.is_empty() {
230 None
231 } else {
232 Some(DerivedConstraint {
233 name: "antiproduct".to_string(),
234 zero_expressions,
235 constraint_grades,
236 })
237 }
238 }
239
240 pub fn derive_all_constraints(
245 &self,
246 ty: &TypeSpec,
247 field_prefix: &str,
248 ) -> Option<DerivedConstraint> {
249 let geometric = self.derive_geometric_constraint(ty, field_prefix);
250 let antiproduct = self.derive_antiproduct_constraint(ty, field_prefix);
251
252 match (geometric, antiproduct) {
253 (None, None) => None,
254 (Some(c), None) | (None, Some(c)) => Some(c),
255 (Some(geo), Some(anti)) => {
256 let mut all_exprs = geo.zero_expressions;
258 let mut all_grades = geo.constraint_grades;
259
260 for (expr, grade) in anti
262 .zero_expressions
263 .into_iter()
264 .zip(anti.constraint_grades)
265 {
266 if !self.is_equivalent_to_any(&expr, &all_exprs) {
267 all_exprs.push(expr);
268 all_grades.push(grade);
269 }
270 }
271
272 if all_exprs.is_empty() {
273 None
274 } else {
275 Some(DerivedConstraint {
276 name: "combined".to_string(),
277 zero_expressions: all_exprs,
278 constraint_grades: all_grades,
279 })
280 }
281 }
282 }
283 }
284
285 fn is_equivalent_to_any(&self, expr: &Atom, existing: &[Atom]) -> bool {
289 use crate::symbolic::ExpressionSimplifier;
290 let simplifier = ExpressionSimplifier::new();
291
292 for other in existing {
293 let diff = expr - other;
294 let simplified = simplifier.simplify(&diff);
295 if self.is_zero(&simplified) {
296 return true;
297 }
298 let neg_diff = expr + other;
300 let neg_simplified = simplifier.simplify(&neg_diff);
301 if self.is_zero(&neg_simplified) {
302 return true;
303 }
304 }
305 false
306 }
307
308 fn compute_antiproduct_antireverse_at_grade(
312 &self,
313 ty: &TypeSpec,
314 output_grade: usize,
315 symbols: &HashMap<usize, Atom>,
316 ) -> Atom {
317 let dim = self.algebra.dim();
318 let pseudoscalar = (1 << dim) - 1; let mut terms: Vec<Atom> = Vec::new();
320
321 for field_a in &ty.fields {
322 for field_b in &ty.fields {
323 let blade_a = field_a.blade_index;
324 let blade_b = field_b.blade_index;
325 let grade_b = Blade::from_index(blade_b).grade();
326
327 let dual_a = pseudoscalar ^ blade_a;
329 let dual_b = pseudoscalar ^ blade_b;
330
331 let (product_sign, dual_result) = self.table.geometric(dual_a, dual_b);
333
334 let result_blade = pseudoscalar ^ dual_result;
336
337 let result_grade = grade(result_blade);
339 if result_grade != output_grade || product_sign == 0 {
340 continue;
341 }
342
343 let antirev_sign = antireverse_sign(grade_b, dim);
345 let total_sign = product_sign * antirev_sign;
346
347 let sym_a = symbols.get(&blade_a).unwrap();
349 let sym_b = symbols.get(&blade_b).unwrap();
350
351 let product = sym_a * sym_b;
353 let term = if total_sign > 0 { product } else { -product };
354 terms.push(term);
355 }
356 }
357
358 if terms.is_empty() {
359 Atom::num(0)
360 } else {
361 terms.sort_by_cached_key(|t| t.to_string());
363 terms.into_iter().reduce(|acc, t| acc + t).unwrap()
364 }
365 }
366
367 pub fn derive_norm_squared(&self, ty: &TypeSpec, field_prefix: &str) -> Atom {
371 let symbols = self.create_symbols(ty, field_prefix);
372 self.compute_product_involution_at_grade(ty, 0, &symbols)
373 }
374
375 pub fn derive_antinorm_squared(&self, ty: &TypeSpec, field_prefix: &str) -> Atom {
379 let dim = self.algebra.dim();
380 let symbols = self.create_symbols(ty, field_prefix);
381 self.compute_antiproduct_antireverse_at_grade(ty, dim, &symbols)
382 }
383
384 pub fn derive_weight_norm_squared(&self, ty: &TypeSpec, field_prefix: &str) -> Atom {
392 let symbols = self.create_symbols(ty, field_prefix);
393 let degenerate_indices: Vec<_> = self.algebra.degenerate_indices().collect();
395
396 let mut terms: Vec<Atom> = Vec::new();
397
398 for field in &ty.fields {
399 let blade = field.blade_index;
401 let is_weight = degenerate_indices
402 .iter()
403 .any(|&idx| (blade & (1 << idx)) != 0);
404
405 if is_weight {
406 if let Some(sym) = symbols.get(&blade) {
407 terms.push(sym * sym);
409 }
410 }
411 }
412
413 if terms.is_empty() {
414 Atom::num(0)
415 } else {
416 terms.sort_by_cached_key(|t| t.to_string());
418 terms.into_iter().reduce(|acc, t| acc + t).unwrap()
419 }
420 }
421
422 pub fn derive_bulk_norm_squared(&self, ty: &TypeSpec, field_prefix: &str) -> Atom {
429 let symbols = self.create_symbols(ty, field_prefix);
430 let degenerate_indices: Vec<_> = self.algebra.degenerate_indices().collect();
432
433 let mut terms: Vec<Atom> = Vec::new();
434
435 for field in &ty.fields {
436 let blade = field.blade_index;
438 let is_bulk = degenerate_indices
439 .iter()
440 .all(|&idx| (blade & (1 << idx)) == 0);
441
442 if is_bulk {
443 if let Some(sym) = symbols.get(&blade) {
444 terms.push(sym * sym);
446 }
447 }
448 }
449
450 if terms.is_empty() {
451 Atom::num(0)
452 } else {
453 terms.sort_by_cached_key(|t| t.to_string());
455 terms.into_iter().reduce(|acc, t| acc + t).unwrap()
456 }
457 }
458
459 pub fn derive_weight_components(&self, ty: &TypeSpec, field_prefix: &str) -> Vec<Atom> {
465 let symbols = self.create_symbols(ty, field_prefix);
466 let degenerate_indices: Vec<_> = self.algebra.degenerate_indices().collect();
468
469 let mut components = Vec::new();
470
471 for field in &ty.fields {
472 let blade = field.blade_index;
474 let is_weight = degenerate_indices
475 .iter()
476 .any(|&idx| (blade & (1 << idx)) != 0);
477
478 if is_weight {
479 if let Some(sym) = symbols.get(&blade) {
480 components.push(sym.clone());
481 }
482 }
483 }
484
485 components
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::spec::parse_spec;
493
494 #[test]
495 fn symbolica_rotor_has_no_derived_constraints() {
496 let spec = parse_spec(
499 r#"
500 [algebra]
501 name = "test"
502 complete = false
503
504 [signature]
505 positive = ["e1", "e2", "e3"]
506
507 [types.Rotor]
508 grades = [0, 2]
509 field_map = [
510 { name = "s", blade = "s" },
511 { name = "xy", blade = "e12" },
512 { name = "xz", blade = "e13" },
513 { name = "yz", blade = "e23" }
514 ]
515 "#,
516 )
517 .unwrap();
518
519 let algebra = Algebra::euclidean(3);
520 let deriver = ConstraintDeriver::new(&algebra, InvolutionKind::Reverse);
521 let rotor = spec.types.iter().find(|t| t.name == "Rotor").unwrap();
522
523 let constraint = deriver.derive_geometric_constraint(rotor, "u");
524
525 assert!(
528 constraint.is_none(),
529 "Euclidean rotor should have no derived geometric constraint"
530 );
531 }
532
533 #[test]
534 fn symbolica_vector_norm_squared() {
535 let spec = parse_spec(
536 r#"
537 [algebra]
538 name = "test"
539 complete = false
540
541 [signature]
542 positive = ["e1", "e2", "e3"]
543
544 [types.Vector]
545 grades = [1]
546 field_map = [
547 { name = "x", blade = "e1" },
548 { name = "y", blade = "e2" },
549 { name = "z", blade = "e3" }
550 ]
551 "#,
552 )
553 .unwrap();
554
555 let algebra = Algebra::euclidean(3);
556 let deriver = ConstraintDeriver::new(&algebra, InvolutionKind::Reverse);
557 let vector = spec.types.iter().find(|t| t.name == "Vector").unwrap();
558
559 let norm_sq = deriver.derive_norm_squared(vector, "v");
560
561 let expr_str = format!("{}", norm_sq);
563 assert!(expr_str.contains("v_x"), "norm² should contain v_x");
564 assert!(expr_str.contains("v_y"), "norm² should contain v_y");
565 assert!(expr_str.contains("v_z"), "norm² should contain v_z");
566 }
567
568 #[test]
569 fn symbolica_pga_motor_has_study_condition() {
570 let spec = parse_spec(
573 r#"
574 [algebra]
575 name = "pga3"
576 complete = false
577
578 [signature]
579 positive = ["e1", "e2", "e3"]
580 zero = ["e0"]
581
582 [types.Motor]
583 grades = [0, 2, 4]
584 field_map = [
585 { name = "s", blade = "s" },
586 { name = "e01", blade = "e14" },
587 { name = "e02", blade = "e24" },
588 { name = "e03", blade = "e34" },
589 { name = "e12", blade = "e12" },
590 { name = "e31", blade = "e13" },
591 { name = "e23", blade = "e23" },
592 { name = "e0123", blade = "e1234" }
593 ]
594 "#,
595 )
596 .unwrap();
597
598 let algebra = Algebra::pga(3);
599 let deriver = ConstraintDeriver::new(&algebra, InvolutionKind::Reverse);
600 let motor = spec.types.iter().find(|t| t.name == "Motor").unwrap();
601
602 let constraint = deriver.derive_geometric_constraint(motor, "m");
603
604 assert!(
606 constraint.is_some(),
607 "PGA motor should have derived geometric constraint (Study condition)"
608 );
609
610 let constraint = constraint.unwrap();
611 assert!(
613 constraint.constraint_grades.contains(&4),
614 "Study condition should involve grade 4 (pseudoscalar)"
615 );
616 }
617
618 #[test]
619 fn symbolica_pga_bivector_has_plucker_condition() {
620 let spec = parse_spec(
623 r#"
624 [algebra]
625 name = "pga3"
626 complete = false
627
628 [signature]
629 positive = ["e1", "e2", "e3"]
630 zero = ["e0"]
631
632 [types.Line]
633 grades = [2]
634 field_map = [
635 { name = "e01", blade = "e14" },
636 { name = "e02", blade = "e24" },
637 { name = "e03", blade = "e34" },
638 { name = "e12", blade = "e12" },
639 { name = "e31", blade = "e13" },
640 { name = "e23", blade = "e23" }
641 ]
642 "#,
643 )
644 .unwrap();
645
646 let algebra = Algebra::pga(3);
647 let deriver = ConstraintDeriver::new(&algebra, InvolutionKind::Reverse);
648 let line = spec.types.iter().find(|t| t.name == "Line").unwrap();
649
650 let constraint = deriver.derive_geometric_constraint(line, "l");
651
652 assert!(
655 constraint.is_some(),
656 "PGA line should have derived geometric constraint (Plücker condition)"
657 );
658 }
659
660 #[test]
661 fn symbolica_euclidean_rotor_all_constraints_deduplicated() {
662 let spec = parse_spec(
665 r#"
666 [algebra]
667 name = "test"
668 complete = false
669
670 [signature]
671 positive = ["e1", "e2", "e3"]
672
673 [types.Rotor]
674 grades = [0, 2]
675 field_map = [
676 { name = "s", blade = "s" },
677 { name = "xy", blade = "e12" },
678 { name = "xz", blade = "e13" },
679 { name = "yz", blade = "e23" }
680 ]
681 "#,
682 )
683 .unwrap();
684
685 let algebra = Algebra::euclidean(3);
686 let deriver = ConstraintDeriver::new(&algebra, InvolutionKind::Reverse);
687 let rotor = spec.types.iter().find(|t| t.name == "Rotor").unwrap();
688
689 let all_constraints = deriver.derive_all_constraints(rotor, "u");
690
691 assert!(
693 all_constraints.is_none(),
694 "Euclidean rotor should have no combined constraints"
695 );
696 }
697
698 #[test]
699 fn symbolica_pga_motor_all_constraints() {
700 let spec = parse_spec(
703 r#"
704 [algebra]
705 name = "pga3"
706 complete = false
707
708 [signature]
709 positive = ["e1", "e2", "e3"]
710 zero = ["e0"]
711
712 [types.Motor]
713 grades = [0, 2, 4]
714 field_map = [
715 { name = "s", blade = "s" },
716 { name = "e01", blade = "e14" },
717 { name = "e02", blade = "e24" },
718 { name = "e03", blade = "e34" },
719 { name = "e12", blade = "e12" },
720 { name = "e31", blade = "e13" },
721 { name = "e23", blade = "e23" },
722 { name = "e0123", blade = "e1234" }
723 ]
724 "#,
725 )
726 .unwrap();
727
728 let algebra = Algebra::pga(3);
729 let deriver = ConstraintDeriver::new(&algebra, InvolutionKind::Reverse);
730 let motor = spec.types.iter().find(|t| t.name == "Motor").unwrap();
731
732 let all_constraints = deriver.derive_all_constraints(motor, "m");
733
734 assert!(
736 all_constraints.is_some(),
737 "PGA motor should have combined constraints"
738 );
739
740 let constraint = all_constraints.unwrap();
741 eprintln!(
743 "PGA Motor combined constraint grades: {:?}",
744 constraint.constraint_grades
745 );
746 eprintln!(
747 "Number of constraint expressions: {}",
748 constraint.zero_expressions.len()
749 );
750 }
751}