1pub mod canon;
17pub mod dummy;
18pub mod graph;
19pub mod spec;
20pub mod young;
21
22use crate::{Atom, AtomArena, Symbol};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum IndexPosition {
28 Upper,
30 Lower,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub struct IndexSlot<'a> {
37 label: Atom<'a>,
39 position: IndexPosition,
41}
42
43impl<'a> IndexSlot<'a> {
44 pub fn new(label: Atom<'a>, position: IndexPosition) -> Self {
46 Self { label, position }
47 }
48
49 pub fn label(&self) -> Atom<'a> {
51 self.label
52 }
53
54 pub fn position(&self) -> IndexPosition {
56 self.position
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Symmetry {
63 None,
65 Symmetric,
67 Antisymmetric,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct Tensor<'a> {
75 name: Symbol,
76 slots: Vec<IndexSlot<'a>>,
77 symmetry: Symmetry,
78}
79
80impl<'a> Tensor<'a> {
81 pub fn new(name: Symbol, slots: Vec<IndexSlot<'a>>) -> Self {
83 Self {
84 name,
85 slots,
86 symmetry: Symmetry::None,
87 }
88 }
89
90 pub fn with_symmetry(mut self, symmetry: Symmetry) -> Self {
92 self.symmetry = symmetry;
93 self
94 }
95
96 pub fn name(&self) -> Symbol {
98 self.name
99 }
100
101 pub fn slots(&self) -> &[IndexSlot<'a>] {
103 &self.slots
104 }
105
106 pub fn symmetry(&self) -> Symmetry {
108 self.symmetry
109 }
110
111 pub fn rank(&self) -> usize {
113 self.slots.len()
114 }
115
116 pub fn dummy_labels(&self) -> Vec<Atom<'a>> {
120 dummies(self.slots().iter().map(|s| s.label()))
121 }
122
123 pub fn to_atom(&self, ctx: &'a AtomArena<'a>) -> Atom<'a> {
127 let args: Vec<Atom<'a>> = self.slots.iter().map(|s| s.label).collect();
128 ctx.fun(self.name.as_str(), &args)
129 }
130}
131
132fn dummies<'a, I: IntoIterator<Item = Atom<'a>>>(labels: I) -> Vec<Atom<'a>> {
136 use crate::FastHashMap;
137 let mut counts: FastHashMap<AtomId<'a>, usize> = FastHashMap::default();
138 for l in labels {
139 let id = AtomId(l);
140 *counts.entry(id).or_insert(0) += 1;
141 }
142 let mut out: Vec<Atom<'a>> = Vec::new();
143 let mut seen: std::collections::HashSet<*const ()> = std::collections::HashSet::new();
144 for (id, n) in counts.iter() {
145 if *n == 2 {
146 let ptr = id.0.node() as *const _ as *const ();
147 if seen.insert(ptr) {
148 out.push(id.0);
149 }
150 }
151 }
152 out
153}
154
155#[derive(Clone, Copy, PartialEq, Eq, Hash)]
156struct AtomId<'a>(Atom<'a>);
157
158#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum Contracted<'a> {
162 Product(TensorProduct<'a>),
165 Scalar(Atom<'a>),
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct TensorProduct<'a> {
173 pub factors: Vec<Tensor<'a>>,
175}
176
177pub fn contract<'a>(ctx: &'a AtomArena<'a>, a: &Tensor<'a>, b: &Tensor<'a>) -> Contracted<'a> {
183 let mut used_a = vec![false; a.slots.len()];
185 let mut used_b = vec![false; b.slots.len()];
186 let mut pair_labels: Vec<Atom<'a>> = Vec::new();
187 for (i, sa) in a.slots.iter().enumerate() {
188 if used_a[i] {
189 continue;
190 }
191 for (j, sb) in b.slots.iter().enumerate() {
192 if used_b[j] {
193 continue;
194 }
195 if sa.label == sb.label && sa.position != sb.position {
196 used_a[i] = true;
197 used_b[j] = true;
198 pair_labels.push(sa.label);
199 break;
200 }
201 }
202 }
203 let mut free: Vec<IndexSlot<'a>> = Vec::new();
205 for (i, s) in a.slots.iter().enumerate() {
206 if !used_a[i] {
207 free.push(*s);
208 }
209 }
210 for (j, s) in b.slots.iter().enumerate() {
211 if !used_b[j] {
212 free.push(*s);
213 }
214 }
215 if pair_labels.is_empty() {
216 return Contracted::Product(TensorProduct {
218 factors: vec![a.clone(), b.clone()],
219 });
220 }
221 if free.is_empty() {
222 let a_atom = a.to_atom(ctx);
224 let b_atom = b.to_atom(ctx);
225 let product = ctx.mul(&[a_atom, b_atom]);
226 return Contracted::Scalar(product);
227 }
228 let name = Symbol::new(&format!("{}_contract_{}", a.name.as_str(), b.name.as_str()));
230 Contracted::Product(TensorProduct {
231 factors: vec![Tensor::new(name, free)],
232 })
233}
234
235pub fn symmetrise_sign(tensor: &Tensor<'_>) -> i64 {
246 match tensor.symmetry {
247 Symmetry::None | Symmetry::Symmetric => 1,
248 Symmetry::Antisymmetric => {
249 let mut slots: Vec<IndexSlot<'_>> = tensor.slots.to_vec();
250 let mut swaps = 0usize;
251 for i in 1..slots.len() {
252 let mut j = i;
253 while j > 0 && slot_less(&slots[j - 1], &slots[j]) {
254 slots.swap(j - 1, j);
255 swaps += 1;
256 j -= 1;
257 }
258 }
259 if swaps.is_multiple_of(2) { 1 } else { -1 }
260 }
261 }
262}
263
264fn slot_less(a: &IndexSlot<'_>, b: &IndexSlot<'_>) -> bool {
265 let pa = a.label.node() as *const _ as *const ();
266 let pb = b.label.node() as *const _ as *const ();
267 (pa as usize) < (pb as usize) || (pa == pb && (a.position as u8) > (b.position as u8))
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use crate::AtomArena;
274 use crate::AtomNode;
275
276 fn idx<'a>(ctx: &'a AtomArena<'a>, name: &str, pos: IndexPosition) -> IndexSlot<'a> {
277 IndexSlot::new(ctx.var(name), pos)
278 }
279
280 #[test]
281 fn tensor_rank_and_slots() {
282 let arena = crate::Arena::new();
283 let ctx = AtomArena::new(&arena);
284 let t = Tensor::new(
285 Symbol::new("T"),
286 vec![
287 idx(&ctx, "i", IndexPosition::Upper),
288 idx(&ctx, "j", IndexPosition::Lower),
289 ],
290 );
291 assert_eq!(t.rank(), 2);
292 assert_eq!(t.slots().len(), 2);
293 assert_eq!(t.symmetry(), Symmetry::None);
294 }
295
296 #[test]
297 fn dummy_detection_finds_repeated_label() {
298 let arena = crate::Arena::new();
299 let ctx = AtomArena::new(&arena);
300 let t = Tensor::new(
301 Symbol::new("T"),
302 vec![
303 idx(&ctx, "i", IndexPosition::Upper),
304 idx(&ctx, "i", IndexPosition::Lower),
305 ],
306 );
307 let dummies = t.dummy_labels();
308 assert_eq!(dummies.len(), 1);
309 }
310
311 #[test]
312 fn contract_two_tensors_with_one_dummy() {
313 let arena = crate::Arena::new();
314 let ctx = AtomArena::new(&arena);
315 let t = Tensor::new(
316 Symbol::new("T"),
317 vec![
318 idx(&ctx, "i", IndexPosition::Upper),
319 idx(&ctx, "j", IndexPosition::Lower),
320 ],
321 );
322 let u = Tensor::new(
323 Symbol::new("U"),
324 vec![
325 idx(&ctx, "j", IndexPosition::Upper),
326 idx(&ctx, "k", IndexPosition::Lower),
327 ],
328 );
329 match contract(&ctx, &t, &u) {
330 Contracted::Product(p) => {
331 assert_eq!(p.factors.len(), 1);
332 assert_eq!(p.factors[0].rank(), 2);
333 }
334 _ => panic!("expected partial contraction product"),
335 }
336 }
337
338 #[test]
339 fn contract_to_scalar_when_no_free_slots() {
340 let arena = crate::Arena::new();
341 let ctx = AtomArena::new(&arena);
342 let t = Tensor::new(Symbol::new("T"), vec![idx(&ctx, "i", IndexPosition::Upper)]);
343 let u = Tensor::new(Symbol::new("U"), vec![idx(&ctx, "i", IndexPosition::Lower)]);
344 match contract(&ctx, &t, &u) {
345 Contracted::Scalar(atom) => {
346 assert!(matches!(atom.node(), AtomNode::Mul(_)));
347 }
348 _ => panic!("expected scalar contraction"),
349 }
350 }
351
352 #[test]
353 fn no_overlap_yields_plain_product() {
354 let arena = crate::Arena::new();
355 let ctx = AtomArena::new(&arena);
356 let t = Tensor::new(Symbol::new("T"), vec![idx(&ctx, "i", IndexPosition::Upper)]);
357 let u = Tensor::new(Symbol::new("U"), vec![idx(&ctx, "j", IndexPosition::Upper)]);
358 match contract(&ctx, &t, &u) {
359 Contracted::Product(p) => assert_eq!(p.factors.len(), 2),
360 _ => panic!("expected plain product"),
361 }
362 }
363
364 #[test]
365 fn antisymmetric_sign_parity() {
366 let arena = crate::Arena::new();
367 let ctx = AtomArena::new(&arena);
368 let e_ab = Tensor::new(
369 Symbol::new("eps"),
370 vec![
371 idx(&ctx, "a", IndexPosition::Lower),
372 idx(&ctx, "b", IndexPosition::Lower),
373 ],
374 )
375 .with_symmetry(Symmetry::Antisymmetric);
376 let e_ba = Tensor::new(
377 Symbol::new("eps"),
378 vec![
379 idx(&ctx, "b", IndexPosition::Lower),
380 idx(&ctx, "a", IndexPosition::Lower),
381 ],
382 )
383 .with_symmetry(Symmetry::Antisymmetric);
384 let s1 = symmetrise_sign(&e_ab);
385 let s2 = symmetrise_sign(&e_ba);
386 assert!(s1 == 1 || s1 == -1);
387 assert!(s2 == 1 || s2 == -1);
388 assert_eq!(s1, -s2);
389 }
390
391 #[test]
392 fn symmetric_sign_is_always_plus() {
393 let arena = crate::Arena::new();
394 let ctx = AtomArena::new(&arena);
395 let g = Tensor::new(
396 Symbol::new("g"),
397 vec![
398 idx(&ctx, "a", IndexPosition::Lower),
399 idx(&ctx, "b", IndexPosition::Lower),
400 ],
401 )
402 .with_symmetry(Symmetry::Symmetric);
403 assert_eq!(symmetrise_sign(&g), 1);
404 }
405
406 #[test]
407 fn to_atom_round_trips_as_function_node() {
408 let arena = crate::Arena::new();
409 let ctx = AtomArena::new(&arena);
410 let t = Tensor::new(
411 Symbol::new("T"),
412 vec![
413 idx(&ctx, "i", IndexPosition::Upper),
414 idx(&ctx, "j", IndexPosition::Lower),
415 ],
416 );
417 let atom = t.to_atom(&ctx);
418 match atom.node() {
419 AtomNode::Fun(name, args) => {
420 assert_eq!(name.as_str(), "T");
421 assert_eq!(args.len(), 2);
422 }
423 _ => panic!("expected Fun node"),
424 }
425 }
426}