Skip to main content

tenferro_einsum/syntax/
nested.rs

1use std::collections::HashSet;
2
3use crate::syntax::notation::{char_to_label, split_and_validate_notation};
4use crate::syntax::subscripts::Subscripts;
5use crate::{Error, Result};
6
7/// Recursive einsum tree that preserves parenthesized grouping.
8///
9/// `NestedEinsum` mirrors OMEinsum.jl's `NestedEinsum`: each internal node
10/// holds [`Subscripts`] describing how its children are contracted, and leaf
11/// nodes reference an original input tensor by index.
12///
13/// # Construction
14///
15/// Use [`NestedEinsum::parse`] to build a tree from parenthesized string
16/// notation such as `"(ij,jk),kl->il"`.  Without parentheses the result is
17/// a flat root node whose children are all leaves.
18///
19/// # Examples
20///
21/// ```
22/// use tenferro_einsum::NestedEinsum;
23///
24/// // Flat (no grouping): root with two leaves
25/// let flat = NestedEinsum::parse("ij,jk->ik").unwrap();
26/// assert!(matches!(flat, NestedEinsum::Node { .. }));
27///
28/// // Grouped: contract first two operands, then with third
29/// let grouped = NestedEinsum::parse("(ij,jk),kl->il").unwrap();
30/// assert!(matches!(grouped, NestedEinsum::Node { .. }));
31/// ```
32#[derive(Debug, Clone)]
33pub enum NestedEinsum {
34    /// A leaf referencing one of the original input tensors by index.
35    Leaf(usize),
36    /// An internal node that contracts its children according to `subscripts`.
37    Node {
38        /// The subscripts for this contraction: one input per child, plus output.
39        subscripts: Subscripts,
40        /// Child sub-expressions (leaves or further nodes).
41        children: Vec<NestedEinsum>,
42    },
43}
44
45impl NestedEinsum {
46    /// Count the total number of leaf operands in the tree.
47    pub fn count_leaves(&self) -> usize {
48        match self {
49            Self::Leaf(_) => 1,
50            Self::Node { children, .. } => children.iter().map(|c| c.count_leaves()).sum(),
51        }
52    }
53
54    /// Parse parenthesized einsum notation into a recursive tree.
55    ///
56    /// Notation follows the standard `"inputs->output"` format with optional
57    /// parentheses to specify contraction order. Each parenthesized group
58    /// becomes an internal [`NestedEinsum::Node`]; bare operands become
59    /// [`NestedEinsum::Leaf`] nodes.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use tenferro_einsum::NestedEinsum;
65    ///
66    /// let nested = NestedEinsum::parse("(ij,jk),kl->il").unwrap();
67    /// // Root has two children: a group node and a leaf
68    /// match &nested {
69    ///     NestedEinsum::Node { children, .. } => assert_eq!(children.len(), 2),
70    ///     _ => panic!("expected Node"),
71    /// }
72    /// ```
73    ///
74    /// # Errors
75    ///
76    /// Returns [`Error::InvalidSubscripts`] when parentheses are mismatched,
77    /// labels are invalid, or the notation is otherwise malformed.
78    pub fn parse(notation: &str) -> Result<Self> {
79        let (lhs, output_str) = split_and_validate_notation(notation)?;
80        if notation.contains('.') {
81            return Err(Error::invalid_subscripts(
82                "ellipsis with parenthesized contraction order is not supported",
83            ));
84        }
85
86        let output: Vec<u32> = output_str
87            .chars()
88            .map(char_to_label)
89            .collect::<Result<_>>()?;
90
91        let mut leaf_counter: usize = 0;
92        let outer_needed: HashSet<u32> = output.iter().copied().collect();
93        Self::parse_group(lhs, &outer_needed, &output, &mut leaf_counter)
94    }
95
96    /// Recursively parse a group (possibly containing sub-groups) into a Node.
97    ///
98    /// `group_str` is a comma-separated list of items (at the top level),
99    /// where each item is either a bare operand (e.g. `"ij"`) or a
100    /// parenthesized sub-group (e.g. `"(ij,jk)"`).
101    ///
102    /// `outer_needed` contains labels that the parent or siblings need from
103    /// this group.  `final_output` is the overall output of the entire
104    /// expression.
105    fn parse_group(
106        group_str: &str,
107        outer_needed: &HashSet<u32>,
108        final_output: &[u32],
109        leaf_counter: &mut usize,
110    ) -> Result<Self> {
111        let items = Self::split_top_level(group_str)?;
112
113        let mut children = Vec::with_capacity(items.len());
114        let mut child_subscript_inputs: Vec<Vec<u32>> = Vec::with_capacity(items.len());
115
116        for (idx, item) in items.iter().enumerate() {
117            if item.starts_with('(') && item.ends_with(')') {
118                // Sub-group: strip outer parens and recurse
119                let inner = &item[1..item.len() - 1];
120
121                // Compute what this sub-group needs to output:
122                // labels in this group that appear in outer_needed or in sibling items
123                let group_labels = Self::collect_labels_in_order(inner)?;
124                let sibling_labels = Self::collect_sibling_labels(&items, idx)?;
125                let mut needed: HashSet<u32> = HashSet::new();
126                let mut sub_output = Vec::new();
127                for label in group_labels {
128                    if outer_needed.contains(&label) || sibling_labels.contains(&label) {
129                        needed.insert(label);
130                        sub_output.push(label);
131                    }
132                }
133
134                let child = Self::parse_group(inner, &needed, &sub_output, leaf_counter)?;
135                child_subscript_inputs.push(sub_output);
136                children.push(child);
137            } else {
138                // Bare operand -> Leaf
139                let labels: Vec<u32> = item.chars().map(char_to_label).collect::<Result<_>>()?;
140                child_subscript_inputs.push(labels);
141                children.push(NestedEinsum::Leaf(*leaf_counter));
142                *leaf_counter += 1;
143            }
144        }
145
146        // Build subscripts for this node
147        let node_output: Vec<u32> = final_output.to_vec();
148        let subscripts = Subscripts {
149            inputs: child_subscript_inputs,
150            output: node_output,
151        };
152
153        Ok(NestedEinsum::Node {
154            subscripts,
155            children,
156        })
157    }
158
159    /// Split a string on commas at the top level (depth 0), respecting parentheses.
160    fn split_top_level(s: &str) -> Result<Vec<&str>> {
161        let mut items = Vec::new();
162        let mut depth: usize = 0;
163        let mut start = 0;
164
165        for (pos, c) in s.char_indices() {
166            match c {
167                '(' => depth += 1,
168                ')' => {
169                    if depth == 0 {
170                        return Err(Error::invalid_subscripts(format!(
171                            "unmatched ')' in einsum group: {s}"
172                        )));
173                    }
174                    depth -= 1;
175                }
176                ',' if depth == 0 => {
177                    items.push(&s[start..pos]);
178                    start = pos + 1; // skip the comma
179                }
180                _ => {}
181            }
182        }
183        // Push the last item
184        items.push(&s[start..]);
185        Ok(items)
186    }
187
188    /// Collect all unique labels from a (possibly nested) string, ignoring
189    /// parentheses and commas.
190    fn collect_labels(s: &str) -> Result<HashSet<u32>> {
191        let mut labels = HashSet::new();
192        for c in s.chars() {
193            match c {
194                '(' | ')' | ',' => continue,
195                _ => {
196                    labels.insert(char_to_label(c)?);
197                }
198            }
199        }
200        Ok(labels)
201    }
202
203    /// Collect unique labels in first-appearance order, ignoring
204    /// parentheses and commas.
205    fn collect_labels_in_order(s: &str) -> Result<Vec<u32>> {
206        let mut seen = HashSet::new();
207        let mut labels = Vec::new();
208        for c in s.chars() {
209            match c {
210                '(' | ')' | ',' => continue,
211                _ => {
212                    let label = char_to_label(c)?;
213                    if seen.insert(label) {
214                        labels.push(label);
215                    }
216                }
217            }
218        }
219        Ok(labels)
220    }
221
222    /// Collect all labels from sibling items (all items except the one at `current_idx`).
223    fn collect_sibling_labels(items: &[&str], current_idx: usize) -> Result<HashSet<u32>> {
224        let mut labels = HashSet::new();
225        for (idx, item) in items.iter().enumerate() {
226            if idx == current_idx {
227                continue;
228            }
229            for label in Self::collect_labels(item)? {
230                labels.insert(label);
231            }
232        }
233        Ok(labels)
234    }
235}
236
237#[cfg(test)]
238mod tests;