use ipfrs_core::{Cid, Result};
use std::collections::HashMap;
use super::streaming::TensorMetadata;
#[derive(Debug, Clone)]
pub struct EinsumExpression {
pub expression: String,
pub inputs: Vec<String>,
pub output: String,
}
impl EinsumExpression {
pub fn parse(expression: impl Into<String>) -> Result<Self> {
let expr = expression.into();
let parts: Vec<&str> = expr.split("->").collect();
if parts.len() != 2 {
return Err(ipfrs_core::error::Error::InvalidInput(
"Invalid einsum expression: missing '->'".to_string(),
));
}
let inputs_str = parts[0];
let output_str = parts[1];
let inputs: Vec<String> = inputs_str
.split(',')
.map(|s| s.trim().to_string())
.collect();
if inputs.is_empty() {
return Err(ipfrs_core::error::Error::InvalidInput(
"Invalid einsum expression: no inputs".to_string(),
));
}
let output = output_str.trim().to_string();
Ok(Self {
expression: expr,
inputs,
output,
})
}
pub fn num_inputs(&self) -> usize {
self.inputs.len()
}
pub fn is_reduction(&self) -> bool {
let output_len = self.output.len();
self.inputs.iter().any(|input| input.len() > output_len)
}
pub fn is_transpose(&self) -> bool {
self.inputs.len() == 1 && self.output.len() == self.inputs[0].len()
}
pub fn shared_indices(&self) -> Vec<char> {
if self.inputs.len() < 2 {
return Vec::new();
}
let first: std::collections::HashSet<char> = self.inputs[0].chars().collect();
let second: std::collections::HashSet<char> = self.inputs[1].chars().collect();
first.intersection(&second).copied().collect()
}
}
#[derive(Debug)]
pub struct EinsumGraph {
expressions: Vec<EinsumExpression>,
tensor_cids: HashMap<String, Cid>,
dependencies: HashMap<String, Vec<String>>,
}
impl EinsumGraph {
pub fn new() -> Self {
Self {
expressions: Vec::new(),
tensor_cids: HashMap::new(),
dependencies: HashMap::new(),
}
}
pub fn add_expression(&mut self, expr: EinsumExpression) {
self.dependencies
.insert(expr.output.clone(), expr.inputs.clone());
self.expressions.push(expr);
}
pub fn register_tensor(&mut self, name: impl Into<String>, cid: Cid) {
self.tensor_cids.insert(name.into(), cid);
}
pub fn tensor_cids(&self) -> &HashMap<String, Cid> {
&self.tensor_cids
}
pub fn get_dependencies(&self, tensor_name: &str) -> Option<Vec<Cid>> {
let dep_names = self.dependencies.get(tensor_name)?;
let cids: Option<Vec<Cid>> = dep_names
.iter()
.map(|name| self.tensor_cids.get(name).copied())
.collect();
cids
}
pub fn topological_order(&self) -> Result<Vec<(String, Cid)>> {
let mut visited = std::collections::HashSet::new();
let mut order = Vec::new();
fn visit(
node: &str,
dependencies: &HashMap<String, Vec<String>>,
tensor_cids: &HashMap<String, Cid>,
visited: &mut std::collections::HashSet<String>,
order: &mut Vec<(String, Cid)>,
) -> Result<()> {
if visited.contains(node) {
return Ok(());
}
visited.insert(node.to_string());
if let Some(deps) = dependencies.get(node) {
for dep in deps {
visit(dep, dependencies, tensor_cids, visited, order)?;
}
}
if let Some(cid) = tensor_cids.get(node) {
order.push((node.to_string(), *cid));
}
Ok(())
}
for tensor_name in self.tensor_cids.keys() {
visit(
tensor_name,
&self.dependencies,
&self.tensor_cids,
&mut visited,
&mut order,
)?;
}
Ok(order)
}
pub fn compute_priority(&self, tensor_name: &str) -> i32 {
let depth = self.compute_depth(tensor_name);
1000 - (depth as i32 * 100)
}
fn compute_depth(&self, tensor_name: &str) -> usize {
if let Some(deps) = self.dependencies.get(tensor_name) {
if deps.is_empty() {
return 0;
}
let max_dep_depth = deps
.iter()
.map(|d| self.compute_depth(d))
.max()
.unwrap_or(0);
max_dep_depth + 1
} else {
0 }
}
pub fn generate_metadata(&self, tensor_name: &str) -> Option<TensorMetadata> {
let cid = *self.tensor_cids.get(tensor_name)?;
let deps = self.get_dependencies(tensor_name).unwrap_or_default();
let priority = self.compute_priority(tensor_name);
Some(
TensorMetadata::new(cid)
.with_dependencies(deps)
.with_priority_hint(priority),
)
}
}
impl Default for EinsumGraph {
fn default() -> Self {
Self::new()
}
}