use crate::{Atom, AtomArena, AtomNode};
#[derive(Debug, Clone)]
pub struct YoungTableau {
pub row_lengths: Vec<usize>,
}
impl YoungTableau {
pub fn new(row_lengths: Vec<usize>) -> Self {
Self { row_lengths }
}
pub fn total_boxes(&self) -> usize {
self.row_lengths.iter().sum()
}
fn sign_of_permutation(&self, perm: &[usize]) -> i64 {
let n = perm.len();
if n != self.total_boxes() {
return 0;
}
let mut row_of = vec![0usize; n];
let mut col_of = vec![0usize; n];
let mut idx = 0;
for (r, &len) in self.row_lengths.iter().enumerate() {
for c in 0..len {
row_of[idx] = r;
col_of[idx] = c;
idx += 1;
}
}
for i in 0..n {
let ri = row_of[i];
let r_len = self.row_lengths[ri];
if r_len > 1 && row_of[perm[i]] != ri {
return 0;
}
}
let mut sign: i64 = 1;
let columns: usize = *self.row_lengths.iter().max().unwrap_or(&0);
for c in 0..columns {
let col_positions: Vec<usize> = (0..n).filter(|&i| col_of[i] == c).collect();
if col_positions.len() <= 1 {
continue;
}
let mut restricted: Vec<usize> = Vec::new();
for &p in &col_positions {
let new_pos = perm.iter().position(|&x| x == p).unwrap_or(p);
let _old_idx = col_positions.iter().position(|&cp| cp == p).unwrap();
let new_idx = col_positions.iter().position(|&cp| cp == new_pos).unwrap();
restricted.push(new_idx);
}
let mut visited = vec![false; restricted.len()];
for i in 0..restricted.len() {
if visited[i] {
continue;
}
let mut cycle_len: usize = 0;
let mut cur = i;
while !visited[cur] {
visited[cur] = true;
cycle_len += 1;
cur = restricted[cur];
}
if cycle_len.is_multiple_of(2) {
sign = -sign;
}
}
}
sign
}
}
pub fn young_project<'a>(
ctx: &'a AtomArena<'a>,
tensor_expr: Atom<'a>,
tableau: &YoungTableau,
) -> Atom<'a> {
match tensor_expr.node() {
AtomNode::Fun(name, args) => {
let rank = tableau.total_boxes();
if args.len() != rank {
return tensor_expr;
}
let mut terms: Vec<Atom<'a>> = Vec::new();
let mut perm: Vec<usize> = (0..rank).collect();
let mut c = vec![0usize; rank];
let s0 = tableau.sign_of_permutation(&perm);
if s0 != 0 {
let reordered: Vec<Atom<'a>> = perm.iter().map(|&i| args[i]).collect();
if s0 == 1 {
terms.push(ctx.fun(name.as_str(), &reordered));
} else {
terms.push(ctx.mul(&[ctx.num(-1), ctx.fun(name.as_str(), &reordered)]));
}
}
let mut i = 1;
while i < rank {
if c[i] < i {
if i % 2 == 0 {
perm.swap(0, i);
} else {
perm.swap(c[i], i);
}
let s = tableau.sign_of_permutation(&perm);
if s != 0 {
let reordered: Vec<Atom<'a>> = perm.iter().map(|&j| args[j]).collect();
if s == 1 {
terms.push(ctx.fun(name.as_str(), &reordered));
} else {
terms.push(ctx.mul(&[ctx.num(-1), ctx.fun(name.as_str(), &reordered)]));
}
}
c[i] += 1;
i = 1;
} else {
c[i] = 0;
i += 1;
}
}
if terms.is_empty() {
ctx.num(0)
} else if terms.len() == 1 {
terms.pop().unwrap()
} else {
ctx.add(&terms)
}
}
_ => tensor_expr,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AtomArena;
use ocas_core::arena::Arena;
#[test]
fn antisymmetric_projector_two_slots() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let a = ctx.var("a");
let b = ctx.var("b");
let f_ab = ctx.fun("f", &[a, b]);
let tableau = YoungTableau::new(vec![1, 1]);
let result = young_project(&ctx, f_ab, &tableau);
let s = result.to_string();
assert!(s.contains('-'), "expected subtraction: {s}");
}
#[test]
fn symmetric_projector_two_slots() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let a = ctx.var("a");
let b = ctx.var("b");
let f_ab = ctx.fun("f", &[a, b]);
let tableau = YoungTableau::new(vec![2]);
let result = young_project(&ctx, f_ab, &tableau);
let s = result.to_string();
assert!(s.contains('+'), "expected addition: {s}");
}
#[test]
fn antisymmetric_three_slots_zero() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let a = ctx.var("a");
let b = ctx.var("b");
let c = ctx.var("c");
let f = ctx.fun("f", &[a, b, c]);
let tableau = YoungTableau::new(vec![1, 1, 1]);
let result = young_project(&ctx, f, &tableau);
let s = result.to_string();
assert!(s.contains('+'), "expected sum: {s}");
}
#[test]
fn identity_preserves_single_slot() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let a = ctx.var("a");
let f = ctx.fun("f", &[a]);
let result = young_project(&ctx, f, &YoungTableau::new(vec![1]));
assert_eq!(result.to_string(), "f(a)");
}
#[test]
fn non_tensor_expression_passthrough() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let result = young_project(&ctx, x, &YoungTableau::new(vec![2]));
assert_eq!(result.to_string(), "x");
}
#[test]
fn rank_mismatch_returns_original() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let a = ctx.var("a");
let b = ctx.var("b");
let f = ctx.fun("f", &[a, b]);
let result = young_project(&ctx, f, &YoungTableau::new(vec![1, 1, 1]));
assert_eq!(result.to_string(), "f(a, b)");
}
#[test]
fn total_boxes_returns_rank() {
let tableau = YoungTableau::new(vec![2, 1]);
assert_eq!(tableau.total_boxes(), 3);
let tableau2 = YoungTableau::new(vec![1, 1, 1]);
assert_eq!(tableau2.total_boxes(), 3);
}
}