1use crate::{Atom, AtomArena, AtomNode};
10
11#[derive(Debug, Clone)]
20pub struct YoungTableau {
21 pub row_lengths: Vec<usize>,
23}
24
25impl YoungTableau {
26 pub fn new(row_lengths: Vec<usize>) -> Self {
29 Self { row_lengths }
30 }
31
32 pub fn total_boxes(&self) -> usize {
34 self.row_lengths.iter().sum()
35 }
36}
37
38fn box_permutations(boxes: &[usize], rank: usize) -> Vec<(Vec<usize>, i64)> {
44 fn build_map(boxes: &[usize], perm: &[usize], rank: usize) -> Vec<usize> {
45 let mut map: Vec<usize> = (0..rank).collect();
46 for (slot, &b) in boxes.iter().enumerate() {
47 map[b] = perm[slot];
48 }
49 map
50 }
51 let m = boxes.len();
52 if m == 0 {
53 return Vec::new();
54 }
55 let mut out = Vec::with_capacity(factorial(m));
56 let mut perm: Vec<usize> = boxes.to_vec();
57 let mut c = vec![0usize; m];
58 let mut sign: i64 = 1;
59 out.push((build_map(boxes, &perm, rank), sign));
60 let mut i = 1;
62 while i < m {
63 if c[i] < i {
64 if i % 2 == 0 {
65 perm.swap(0, i);
66 } else {
67 perm.swap(c[i], i);
68 }
69 sign = -sign;
70 out.push((build_map(boxes, &perm, rank), sign));
71 c[i] += 1;
72 i = 1;
73 } else {
74 c[i] = 0;
75 i += 1;
76 }
77 }
78 out
79}
80
81fn factorial(n: usize) -> usize {
82 (1..=n).product()
83}
84
85pub fn young_project<'a>(
98 ctx: &'a AtomArena<'a>,
99 tensor_expr: Atom<'a>,
100 tableau: &YoungTableau,
101) -> Atom<'a> {
102 match tensor_expr.node() {
103 AtomNode::Fun(name, args) => {
104 let rank = tableau.total_boxes();
105 if args.len() != rank {
106 return tensor_expr;
107 }
108
109 let mut rows: Vec<Vec<usize>> = Vec::new();
111 let mut idx = 0usize;
112 for &len in &tableau.row_lengths {
113 rows.push((idx..idx + len).collect());
114 idx += len;
115 }
116 let columns = tableau.row_lengths.iter().copied().max().unwrap_or(0);
118 let mut cols: Vec<Vec<usize>> = Vec::new();
119 for c in 0..columns {
120 let mut col = Vec::new();
121 let mut k = 0usize;
122 for &len in &tableau.row_lengths {
123 if c < len {
124 col.push(k + c);
125 }
126 k += len;
127 }
128 if !col.is_empty() {
129 cols.push(col);
130 }
131 }
132
133 let mut row_stack: Vec<(Vec<usize>, i64)> = vec![((0..rank).collect(), 1)];
135 for row in &rows {
136 let perms = box_permutations(row, rank);
137 let mut next = Vec::new();
138 for (m1, s1) in &row_stack {
139 for (p, s2) in &perms {
140 let mut composed = vec![0usize; rank];
141 for j in 0..rank {
142 composed[j] = m1[p[j]];
143 }
144 next.push((composed, s1 * s2));
145 }
146 }
147 row_stack = next;
148 }
149 let mut col_stack: Vec<(Vec<usize>, i64)> = vec![((0..rank).collect(), 1)];
151 for col in &cols {
152 let perms = box_permutations(col, rank);
153 let mut next = Vec::new();
154 for (m1, s1) in &col_stack {
155 for (p, s2) in &perms {
156 let mut composed = vec![0usize; rank];
157 for j in 0..rank {
158 composed[j] = m1[p[j]];
159 }
160 next.push((composed, s1 * s2));
161 }
162 }
163 col_stack = next;
164 }
165
166 let mut terms: Vec<Atom<'a>> = Vec::new();
167 for (tau, sign_tau) in &col_stack {
168 for (sigma, _) in &row_stack {
169 let mut perm = vec![0usize; rank];
172 for j in 0..rank {
173 perm[tau[sigma[j]]] = j;
174 }
175 let reordered: Vec<Atom<'a>> = perm.iter().map(|&i| args[i]).collect();
176 if *sign_tau == 1 {
177 terms.push(ctx.fun(name.as_str(), &reordered));
178 } else {
179 terms.push(ctx.mul(&[ctx.num(-1), ctx.fun(name.as_str(), &reordered)]));
180 }
181 }
182 }
183
184 if terms.is_empty() {
185 ctx.num(0)
186 } else if terms.len() == 1 {
187 terms.pop().unwrap()
188 } else {
189 ctx.add(&terms)
190 }
191 }
192 _ => tensor_expr,
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::AtomArena;
200 use ocas_core::arena::Arena;
201
202 #[test]
203 fn antisymmetric_projector_two_slots() {
204 let arena = Arena::new();
205 let ctx = AtomArena::new(&arena);
206 let a = ctx.var("a");
207 let b = ctx.var("b");
208 let f_ab = ctx.fun("f", &[a, b]);
209 let tableau = YoungTableau::new(vec![1, 1]);
211 let result = young_project(&ctx, f_ab, &tableau);
212 let s = result.to_string();
213 assert!(s.contains('-'), "expected subtraction: {s}");
215 }
216
217 #[test]
218 fn symmetric_projector_two_slots() {
219 let arena = Arena::new();
220 let ctx = AtomArena::new(&arena);
221 let a = ctx.var("a");
222 let b = ctx.var("b");
223 let f_ab = ctx.fun("f", &[a, b]);
224 let tableau = YoungTableau::new(vec![2]);
226 let result = young_project(&ctx, f_ab, &tableau);
227 let s = result.to_string();
228 assert!(s.contains('+'), "expected addition: {s}");
230 }
231
232 #[test]
233 fn antisymmetric_three_slots_zero() {
234 let arena = Arena::new();
235 let ctx = AtomArena::new(&arena);
236 let a = ctx.var("a");
237 let b = ctx.var("b");
238 let c = ctx.var("c");
239 let f = ctx.fun("f", &[a, b, c]);
240 let tableau = YoungTableau::new(vec![1, 1, 1]);
241 let result = young_project(&ctx, f, &tableau);
242 let s = result.to_string();
243 assert!(s.contains('+'), "expected sum: {s}");
245 }
246
247 #[test]
248 fn identity_preserves_single_slot() {
249 let arena = Arena::new();
250 let ctx = AtomArena::new(&arena);
251 let a = ctx.var("a");
252 let f = ctx.fun("f", &[a]);
253 let result = young_project(&ctx, f, &YoungTableau::new(vec![1]));
255 assert_eq!(result.to_string(), "f(a)");
257 }
258
259 #[test]
260 fn non_tensor_expression_passthrough() {
261 let arena = Arena::new();
262 let ctx = AtomArena::new(&arena);
263 let x = ctx.var("x");
265 let result = young_project(&ctx, x, &YoungTableau::new(vec![2]));
266 assert_eq!(result.to_string(), "x");
267 }
268
269 #[test]
270 fn rank_mismatch_returns_original() {
271 let arena = Arena::new();
272 let ctx = AtomArena::new(&arena);
273 let a = ctx.var("a");
274 let b = ctx.var("b");
275 let f = ctx.fun("f", &[a, b]);
276 let result = young_project(&ctx, f, &YoungTableau::new(vec![1, 1, 1]));
278 assert_eq!(result.to_string(), "f(a, b)");
279 }
280
281 #[test]
282 fn total_boxes_returns_rank() {
283 let tableau = YoungTableau::new(vec![2, 1]);
284 assert_eq!(tableau.total_boxes(), 3);
285 let tableau2 = YoungTableau::new(vec![1, 1, 1]);
286 assert_eq!(tableau2.total_boxes(), 3);
287 }
288
289 #[test]
290 fn mixed_shape_does_not_panic() {
291 let arena = Arena::new();
294 let ctx = AtomArena::new(&arena);
295 let a = ctx.var("a");
296 let b = ctx.var("b");
297 let c = ctx.var("c");
298 let f = ctx.fun("f", &[a, b, c]);
299 let tableau = YoungTableau::new(vec![2, 1]);
300 let result = young_project(&ctx, f, &tableau);
301 let s = result.to_string();
302 assert!(s.contains("f(a, b, c)"), "missing identity term: {s}");
304 assert!(s.contains("f(b, a, c)"), "missing row-swap term: {s}");
305 assert!(s.contains("f(c, b, a)"), "missing column-swap term: {s}");
306 }
307
308 #[test]
309 fn two_by_two_tableau() {
310 let arena = Arena::new();
312 let ctx = AtomArena::new(&arena);
313 let args: Vec<Atom<'_>> = ["a", "b", "c", "d"].iter().map(|&x| ctx.var(x)).collect();
314 let f = ctx.fun("f", &args);
315 let tableau = YoungTableau::new(vec![2, 2]);
316 let result = young_project(&ctx, f, &tableau);
317 let s = result.to_string();
318 assert!(s.contains("f(a, b, c, d)"), "missing identity term: {s}");
319 }
320}