Skip to main content

opt_einsum_path/
parser.rs

1use crate::*;
2
3/// Get the symbol corresponding to int ``i`` - runs through the usual 52 letters before resorting
4/// to unicode characters, starting at ``chr(192)`` and skipping surrogates.
5///
6/// # Examples
7///
8/// ```rust
9/// # use opt_einsum_path::parser::get_symbol;
10/// assert_eq!(get_symbol(2), 'c');
11/// assert_eq!(get_symbol(200), 'Ŕ');
12/// assert_eq!(get_symbol(20000), '京');
13/// ```
14pub fn get_symbol(i: u32) -> char {
15    match i {
16        0..26 => char::from(b'a' + i as u8),
17        26..52 => char::from(b'A' + (i - 26) as u8),
18        52..55296 => char::from_u32(i + 140).unwrap(),
19        55296.. => char::from_u32(i + 2048).unwrap(),
20    }
21}
22
23/// Generate `n` unused symbols not in `used`.
24///
25/// # Examples
26///
27/// ```rust
28/// # use std::collections::BTreeSet;
29/// # use opt_einsum_path::parser::gen_unused_symbols;
30/// let unused = gen_unused_symbols("abd", 2);
31/// assert_eq!(unused, "ce".to_string());
32/// ```
33pub fn gen_unused_symbols(used: &str, n: usize) -> String {
34    let mut unused = Vec::with_capacity(n);
35    let mut i = 0;
36    while unused.len() < n {
37        let c = get_symbol(i);
38        if !used.contains(c) {
39            unused.push(c);
40        }
41        i += 1;
42    }
43    unused.into_iter().collect()
44}
45
46/// Find the output string for the inputs `subscripts` under canonical Einstein summation rules.
47/// That is, repeated indices are summed over by default.
48///
49/// # Examples
50///
51/// ```
52/// # use opt_einsum_path::parser::find_output_str;
53/// assert_eq!(find_output_str("ab,bc"), "ac");
54/// assert_eq!(find_output_str("a,b"), "ab");
55/// assert_eq!(find_output_str("a,a,b,b"), "");
56/// ```
57pub fn find_output_str(subscripts: &str) -> String {
58    let tmp_subscripts: String = subscripts.split(',').collect();
59    let mut unique_chars = BTreeSet::new();
60    let mut repeated_chars = BTreeSet::new();
61
62    // Track which characters appear exactly once
63    for c in tmp_subscripts.chars() {
64        if !unique_chars.insert(c) {
65            repeated_chars.insert(c);
66        }
67    }
68
69    // Collect characters that appear exactly once (not in repeated_chars)
70    (&unique_chars - &repeated_chars).into_iter().collect()
71}
72
73/// Find the output shape for given inputs, shapes and output string, taking
74/// into account broadcasting.
75///
76/// # Examples
77///
78/// ```
79/// # use opt_einsum_path::parser::find_output_shape;
80/// assert_eq!(find_output_shape(&["ab", "bc"], &[vec![2, 3], vec![3, 4]], "ac"), vec![2, 4]);
81/// // Broadcasting is accounted for
82/// assert_eq!(find_output_shape(&["a", "a"], &[vec![4], vec![1]], "a"), vec![4]);
83/// ```
84pub fn find_output_shape(inputs: &[&str], shapes: &[TensorShapeType], output: &str) -> TensorShapeType {
85    output
86        .chars()
87        .map(|c| {
88            shapes
89                .iter()
90                .zip(inputs.iter())
91                .filter_map(|(shape, input)| input.chars().position(|x| x == c).map(|loc| shape.get(loc).copied()))
92                .flatten()
93                .max()
94                .unwrap() // unwrap should be safe for valid einsum
95        })
96        .collect()
97}
98
99/// Convert user custom subscripts list to subscript string according to `symbol_map`.
100///
101/// # Examples
102/// ```
103/// # use std::collections::BTreeMap;
104/// # use opt_einsum_path::parser::convert_subscripts;
105/// let symbol_map = BTreeMap::from([("abc", "a"), ("def", "b")]);
106/// assert_eq!(convert_subscripts(&["abc", "def"], &symbol_map), "ab");
107/// // with ellipsis
108/// let symbol_map = BTreeMap::from([("ast", "a")]);
109/// assert_eq!(convert_subscripts(&["ast", "ast", "..."], &symbol_map), "aa...");
110/// ```
111pub fn convert_subscripts(old_sub: &[&str], symbol_map: &BTreeMap<&str, &str>) -> String {
112    old_sub.iter().map(|&s| if s == "..." { "..." } else { symbol_map[s] }).collect()
113}
114
115/// A reproduction of einsum's parsing logic in Rust (originally from Python/Numpy).
116///
117/// # Arguments
118///
119/// * `subscripts` - The einsum subscript string (e.g., "ij,jk->ik").
120/// * `shapes` - Array shapes or tensors to contract.
121///
122/// # Returns
123///
124/// - `input_subscripts` Parsed input subscripts (e.g., "ij,jk").
125/// - `output_subscript` Parsed output subscript (e.g., "ik").
126/// - `operands` The operands (shapes) to use in contraction.
127///
128/// # Examples
129///
130/// ```rust
131/// # use opt_einsum_path::parser::parse_einsum_input;
132/// let a_shape = vec![4, 4];
133/// let b_shape = vec![4, 4, 4];
134/// let subscripts = "...a, ...a -> ...";
135/// let (input_subscripts, output_subscript, operands) = parse_einsum_input(subscripts, &[a_shape, b_shape]).unwrap();
136/// assert_eq!(input_subscripts, "da,cda");
137/// assert_eq!(output_subscript, "cd");
138/// assert_eq!(operands, vec![vec![4, 4], vec![4, 4, 4]]);
139/// ```
140///
141/// # Errors
142///
143/// - Subscripts contain invalid '->' patterns (larger than one '->', or contains separated `-` or
144///   `>` characters).
145/// - Malformed ellipses ("...").
146/// - Output characters are non-unique or missing from inputs.
147/// - Operand count doesn't match subscript terms.
148pub fn parse_einsum_input(
149    subscripts: &str,
150    operands: &[TensorShapeType],
151) -> Result<(String, String, Vec<TensorShapeType>), String> {
152    // remove whitespace
153    let einsum_str = subscripts.split_whitespace().collect::<String>();
154
155    // Check for proper "->"
156    if (einsum_str.contains('-') || einsum_str.contains('>'))
157        && (einsum_str.matches('-').count() > 1
158            || einsum_str.matches('>').count() > 1
159            || einsum_str.matches("->").count() != 1)
160    {
161        return Err("Subscripts can only contain one '->'.".to_string());
162    }
163
164    // Parse ellipses
165    let mut subscripts = einsum_str.to_string();
166    if einsum_str.contains('.') {
167        let used: String = einsum_str.replace(".", "").replace(",", "").replace("->", "");
168        let ellipse_inds = gen_unused_symbols(&used, operands.iter().map(|s| s.len()).max().unwrap_or(0));
169        let mut longest = 0;
170
171        // Do we have an output to account for?
172        let (split_subscripts, out_sub) = if einsum_str.contains("->") {
173            let parts = einsum_str.split("->").collect_vec();
174            let input_tmp = parts[0];
175            (input_tmp.split(',').collect_vec(), true)
176        } else {
177            (einsum_str.split(',').collect_vec(), false)
178        };
179
180        let mut processed_subscripts = Vec::new();
181        for (num, sub) in split_subscripts.iter().enumerate() {
182            if sub.contains('.') {
183                if sub.matches('.').count() != 3 || !sub.contains("...") {
184                    return Err("Invalid Ellipses.".to_string());
185                }
186
187                // Take into account numerical values
188                let ellipse_count = if operands[num].is_empty() {
189                    0
190                } else if operands[num].len() >= sub.len() - 3 {
191                    operands[num].len() - (sub.len() - 3)
192                } else {
193                    return Err("Ellipses lengths do not match.".to_string());
194                };
195                longest = longest.max(ellipse_count);
196
197                if ellipse_count == 0 {
198                    processed_subscripts.push(sub.replace("...", ""));
199                } else {
200                    let replacement = &ellipse_inds[ellipse_inds.len() - ellipse_count..];
201                    processed_subscripts.push(sub.replace("...", replacement));
202                }
203            } else {
204                processed_subscripts.push(sub.to_string());
205            }
206        }
207
208        subscripts = processed_subscripts.join(",");
209
210        // Figure out output ellipses
211        let out_ellipse = if longest == 0 { "" } else { &ellipse_inds[ellipse_inds.len() - longest..] };
212
213        subscripts = if out_sub {
214            let parts: Vec<&str> = einsum_str.split("->").collect();
215            let output_sub = parts[1];
216            format!("{subscripts}->{}", output_sub.replace("...", out_ellipse))
217        } else {
218            // Special care for outputless ellipses
219            let output_subscript = find_output_str(&subscripts);
220            let normal_inds: String = output_subscript.chars().filter(|c| !out_ellipse.contains(*c)).collect();
221            format!("{subscripts}->{out_ellipse}{normal_inds}")
222        };
223    }
224
225    // Build output string if does not exist
226    let (input_subscripts, output_subscript) = if subscripts.contains("->") {
227        let parts: Vec<&str> = subscripts.split("->").collect();
228        (parts[0].to_string(), parts[1].to_string())
229    } else {
230        (subscripts.clone(), find_output_str(&subscripts))
231    };
232
233    // Make sure output subscripts are unique and in the input
234    for char in output_subscript.chars() {
235        if output_subscript.matches(char).count() != 1 {
236            return Err(format!("Output character '{char}' appeared more than once in the output."));
237        }
238        if !input_subscripts.contains(char) {
239            return Err(format!("Output character '{char}' did not appear in the input"));
240        }
241    }
242
243    // Make sure number operands is equivalent to the number of terms
244    let input_count = input_subscripts.split(',').count();
245    if input_count != operands.len() {
246        return Err(format!(
247            "Number of einsum subscripts, {input_count}, must be equal to the number of operands, {}.",
248            operands.len()
249        ));
250    }
251
252    Ok((input_subscripts, output_subscript, operands.to_vec()))
253}
254
255#[test]
256fn playground() {
257    for i in 0..500 {
258        println!("{} -> '{:?}'", i, char::from_u32(i));
259    }
260}