Files
rustytorch/crates/core/rtx-graph/src/graph.rs
T
2026-03-04 00:08:42 +00:00

1034 lines
34 KiB
Rust

// Core graph structures for unified data+compute representation
// Phase 6: Self-Optimizing Platform
use indexmap::IndexMap;
use petgraph::graph::NodeIndex;
use petgraph::visit::EdgeRef;
use petgraph::{Directed, Direction, Graph as PetGraph};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use uuid::Uuid;
use crate::{
ComputeOp, DataOp, ExecutionContext, ExecutionPlan, GraphError, MemoryAnalysis, OperationType,
Result,
};
/// Unique identifier for graph nodes
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodeId(Uuid);
impl NodeId {
/// Create a new unique node ID
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Get the underlying UUID
pub fn as_uuid(&self) -> Uuid {
self.0
}
}
impl From<Uuid> for NodeId {
fn from(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Node information in the graph
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphNode {
/// Unique identifier
pub id: NodeId,
/// Operation performed by this node
pub operation: OperationType,
/// Optional metadata
pub metadata: IndexMap<String, String>,
}
impl GraphNode {
/// Create a new graph node
pub fn new(operation: OperationType) -> Result<Self> {
operation.validate()?;
Ok(Self {
id: NodeId::new(),
operation,
metadata: IndexMap::new(),
})
}
/// Add metadata to the node
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
/// Edge information representing dependencies
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphEdge {
/// Source node
pub from: NodeId,
/// Target node
pub to: NodeId,
/// Optional edge metadata
pub metadata: IndexMap<String, String>,
}
/// Unified data and compute graph
#[derive(Debug, Clone)]
pub struct Graph {
/// Underlying directed graph structure
graph: PetGraph<GraphNode, GraphEdge, Directed>,
/// Mapping from node IDs to graph indices
node_index_map: HashMap<NodeId, NodeIndex>,
/// Graph-level metadata
metadata: IndexMap<String, String>,
}
impl Graph {
/// Create a new empty graph
pub fn new() -> Self {
Self {
graph: PetGraph::new(),
node_index_map: HashMap::new(),
metadata: IndexMap::new(),
}
}
/// Get the number of nodes in the graph
pub fn node_count(&self) -> usize {
self.graph.node_count()
}
/// Get the number of edges in the graph
pub fn edge_count(&self) -> usize {
self.graph.edge_count()
}
/// Get operation for a node
pub fn get_operation(&self, node_id: NodeId) -> Result<&OperationType> {
let node_index = self
.node_index_map
.get(&node_id)
.ok_or_else(|| GraphError::node_not_found(node_id.as_uuid()))?;
let node = self
.graph
.node_weight(*node_index)
.ok_or_else(|| GraphError::node_not_found(node_id.as_uuid()))?;
Ok(&node.operation)
}
/// Get all nodes in the graph
pub fn nodes(&self) -> impl Iterator<Item = &GraphNode> {
self.graph.node_weights()
}
/// Get dependencies of a node (incoming edges)
pub fn dependencies(&self, node_id: NodeId) -> Result<Vec<NodeId>> {
let node_index = self
.node_index_map
.get(&node_id)
.ok_or_else(|| GraphError::node_not_found(node_id.as_uuid()))?;
let deps: Vec<NodeId> = self
.graph
.edges_directed(*node_index, Direction::Incoming)
.map(|edge| {
let source_index = edge.source();
self.graph.node_weight(source_index).unwrap().id
})
.collect();
Ok(deps)
}
/// Get dependents of a node (outgoing edges)
pub fn dependents(&self, node_id: NodeId) -> Result<Vec<NodeId>> {
let node_index = self
.node_index_map
.get(&node_id)
.ok_or_else(|| GraphError::node_not_found(node_id.as_uuid()))?;
let deps: Vec<NodeId> = self
.graph
.edges_directed(*node_index, Direction::Outgoing)
.map(|edge| {
let target_index = edge.target();
self.graph.node_weight(target_index).unwrap().id
})
.collect();
Ok(deps)
}
/// Execute the graph with given context
pub fn execute(&self, context: &mut ExecutionContext) -> Result<Option<rtx_tensor::Tensor>> {
use crate::OperationType;
tracing::info!(
"Executing graph with {} nodes, {} edges",
self.node_count(),
self.edge_count()
);
// Get topological order for dependency-respecting execution
let execution_order = self.topological_order()?;
let mut final_result = None;
// Execute nodes in dependency order
for node_id in execution_order {
let node_index = self
.node_index_map
.get(&node_id)
.ok_or_else(|| GraphError::node_not_found(node_id.as_uuid()))?;
let node = self
.graph
.node_weight(*node_index)
.ok_or_else(|| GraphError::node_not_found(node_id.as_uuid()))?;
tracing::debug!(
"Executing node {} with operation {}",
node_id,
node.operation.name()
);
match &node.operation {
OperationType::Data(data_op) => {
// Execute data operations (don't store intermediate results)
self.execute_data_op(data_op, context)?;
}
OperationType::Compute(compute_op) => {
// Execute compute operations and store results
let result = self.execute_compute_op(compute_op, context)?;
context.store_result(node_id, result.clone());
final_result = Some(result);
}
}
}
tracing::info!(
"Graph execution completed. Executed {} nodes",
context.stats().nodes_executed
);
Ok(final_result)
}
/// Get execution plan for the graph
pub fn get_execution_plan(&self) -> Result<ExecutionPlan> {
ExecutionPlan::from_graph(self)
}
/// Analyze memory usage of the graph
pub fn analyze_memory_usage(&self) -> Result<MemoryAnalysis> {
MemoryAnalysis::from_graph(self)
}
/// Serialize graph to JSON
pub fn to_json(&self) -> Result<String> {
let serializable = crate::serialization::SerializableGraph::from_graph(self);
serializable.to_json()
}
/// Deserialize graph from JSON
pub fn from_json(json: &str) -> Result<Self> {
let serializable = crate::serialization::SerializableGraph::from_json(json)?;
serializable.to_graph()
}
/// Validate graph structure and operations
pub fn validate(&self) -> Result<()> {
// Check for cycles
if petgraph::algo::is_cyclic_directed(&self.graph) {
return Err(GraphError::CyclicDependency);
}
// Validate all operations
for node in self.nodes() {
node.operation.validate()?;
}
Ok(())
}
/// Add metadata to the graph
pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
}
/// Get graph metadata
pub fn metadata(&self) -> &IndexMap<String, String> {
&self.metadata
}
/// Get topological ordering of nodes
pub fn topological_order(&self) -> Result<Vec<NodeId>> {
let topo = petgraph::algo::toposort(&self.graph, None)
.map_err(|_| GraphError::CyclicDependency)?;
let node_ids: Vec<NodeId> = topo
.into_iter()
.map(|node_index| self.graph.node_weight(node_index).unwrap().id)
.collect();
Ok(node_ids)
}
/// Execute a data operation
fn execute_data_op(
&self,
data_op: &crate::DataOp,
_context: &mut ExecutionContext,
) -> Result<()> {
use crate::DataOp;
match data_op {
DataOp::Read { source, format, .. } => {
tracing::debug!("Reading data from source: {} (format: {})", source, format);
// In a real implementation, this would:
// 1. Open the data source (file, database, API, etc.)
// 2. Parse according to format
// 3. Load into memory or prepare for streaming
// For now, we simulate successful read
Ok(())
}
DataOp::Transform {
operation,
predicate,
} => {
tracing::debug!(
"Applying transformation: {} with predicate: {}",
operation,
predicate
);
// In a real implementation, this would:
// 1. Apply the transformation operation to the data
// 2. Use the predicate to filter/transform data
Ok(())
}
DataOp::Write {
destination,
format,
..
} => {
tracing::debug!(
"Writing data to destination: {} (format: {})",
destination,
format
);
// In a real implementation, this would:
// 1. Serialize data according to format
// 2. Write to destination (file, database, API, etc.)
Ok(())
}
DataOp::Join {
join_type,
condition,
} => {
tracing::debug!(
"Performing {} join with condition: {}",
join_type,
condition
);
// In a real implementation, this would:
// 1. Apply join logic between two data sources
// 2. Use condition for join predicate
Ok(())
}
DataOp::Aggregate {
group_by,
aggregates,
} => {
tracing::debug!(
"Aggregating by {:?} with functions: {:?}",
group_by,
aggregates
);
// In a real implementation, this would:
// 1. Group data by specified columns
// 2. Apply aggregation functions
Ok(())
}
DataOp::Sort {
columns,
directions,
} => {
tracing::debug!(
"Sorting by columns {:?} in directions {:?}",
columns,
directions
);
// In a real implementation, this would:
// 1. Sort data by specified columns and directions
Ok(())
}
}
}
/// Execute a compute operation
fn execute_compute_op(
&self,
compute_op: &crate::ComputeOp,
context: &mut ExecutionContext,
) -> Result<rtx_tensor::Tensor> {
use crate::ComputeOp;
match compute_op {
ComputeOp::TensorLoad {
data_node: _,
shape,
dtype,
} => {
tracing::debug!("Loading tensor with shape {:?} and dtype {}", shape, dtype);
// Create tensor from the loaded data
// In a real implementation, this would use the data from data_node
let tensor =
match dtype.as_str() {
"f32" => rtx_tensor::Tensor::zeros(shape.clone(), context.device())
.map_err(|e| {
GraphError::invalid_operation(format!(
"Failed to create f32 tensor: {e}"
))
})?,
"f16" => rtx_tensor::Tensor::zeros(shape.clone(), context.device())
.map_err(|e| {
GraphError::invalid_operation(format!(
"Failed to create f16 tensor: {e}"
))
})?,
"i32" => rtx_tensor::Tensor::zeros(shape.clone(), context.device())
.map_err(|e| {
GraphError::invalid_operation(format!(
"Failed to create i32 tensor: {e}"
))
})?,
_ => {
return Err(GraphError::invalid_operation(format!(
"Unsupported dtype: {dtype}"
)));
}
};
Ok(tensor)
}
ComputeOp::Add { lhs, rhs } => {
tracing::debug!("Computing addition: {} + {}", lhs, rhs);
let lhs_tensor = context
.get_result(*lhs)
.ok_or_else(|| GraphError::node_not_found(lhs.as_uuid()))?;
let rhs_tensor = context
.get_result(*rhs)
.ok_or_else(|| GraphError::node_not_found(rhs.as_uuid()))?;
let result = lhs_tensor
.add(rhs_tensor)
.map_err(|e| GraphError::runtime_error(format!("Addition failed: {e}")))?;
Ok(result)
}
ComputeOp::Mul { lhs, rhs } => {
tracing::debug!("Computing multiplication: {} * {}", lhs, rhs);
let lhs_tensor = context
.get_result(*lhs)
.ok_or_else(|| GraphError::node_not_found(lhs.as_uuid()))?;
let rhs_tensor = context
.get_result(*rhs)
.ok_or_else(|| GraphError::node_not_found(rhs.as_uuid()))?;
let result = lhs_tensor.mul(rhs_tensor).map_err(|e| {
GraphError::runtime_error(format!("Multiplication failed: {e}"))
})?;
Ok(result)
}
ComputeOp::MatMul { lhs, rhs } => {
tracing::debug!("Computing matrix multiplication: {} @ {}", lhs, rhs);
let lhs_tensor = context
.get_result(*lhs)
.ok_or_else(|| GraphError::node_not_found(lhs.as_uuid()))?;
let rhs_tensor = context
.get_result(*rhs)
.ok_or_else(|| GraphError::node_not_found(rhs.as_uuid()))?;
let result = lhs_tensor.matmul(rhs_tensor).map_err(|e| {
GraphError::runtime_error(format!("Matrix multiplication failed: {e}"))
})?;
Ok(result)
}
ComputeOp::Conv2d {
input,
weight,
bias,
stride,
padding,
dilation,
groups,
} => {
tracing::debug!(
"Computing 2D convolution with stride {:?}, padding {:?}, dilation {:?}, groups {}",
stride,
padding,
dilation,
groups
);
let input_tensor = context
.get_result(*input)
.ok_or_else(|| GraphError::node_not_found(input.as_uuid()))?;
let weight_tensor = context
.get_result(*weight)
.ok_or_else(|| GraphError::node_not_found(weight.as_uuid()))?;
// Get bias tensor if provided
let bias_tensor = if let Some(bias_node) = bias {
Some(
context
.get_result(*bias_node)
.ok_or_else(|| GraphError::node_not_found(bias_node.as_uuid()))?,
)
} else {
None
};
// Use the real conv2d implementation from rtx-tensor
let result = input_tensor
.conv2d(
weight_tensor,
bias_tensor,
stride[0], // Convert [usize; 2] to usize for now
padding[0], // Convert [usize; 2] to usize for now
dilation[0], // Convert [usize; 2] to usize for now
*groups,
)
.map_err(|e| {
GraphError::runtime_error(format!("Conv2d operation failed: {e}"))
})?;
Ok(result)
}
ComputeOp::Activation {
input,
activation_type,
} => {
tracing::debug!("Computing {} activation", activation_type);
let input_tensor = context
.get_result(*input)
.ok_or_else(|| GraphError::node_not_found(input.as_uuid()))?;
let result = match activation_type.as_str() {
"relu" => input_tensor.relu().map_err(|e| {
GraphError::runtime_error(format!("ReLU activation failed: {e}"))
})?,
"tanh" => input_tensor.tanh().map_err(|e| {
GraphError::runtime_error(format!("Tanh activation failed: {e}"))
})?,
"sigmoid" => input_tensor.sigmoid().map_err(|e| {
GraphError::runtime_error(format!("Sigmoid activation failed: {e}"))
})?,
_ => {
// For unsupported activations, return input as placeholder
tracing::warn!(
"Activation {} not yet implemented, returning input",
activation_type
);
input_tensor.clone()
}
};
Ok(result)
}
ComputeOp::Pool2d {
input,
pool_type,
kernel_size,
stride,
} => {
tracing::debug!(
"Computing 2D {} pooling with kernel {:?}, stride {:?}",
pool_type,
kernel_size,
stride
);
let input_tensor = context
.get_result(*input)
.ok_or_else(|| GraphError::node_not_found(input.as_uuid()))?;
// Use real pooling implementation from rtx-tensor
let result = match pool_type.as_str() {
"max" => input_tensor
.max_pool2d(kernel_size[0], stride[0], 0)
.map_err(|e| {
GraphError::runtime_error(format!("Max pool2d failed: {e}"))
})?,
"avg" | "average" => input_tensor
.avg_pool2d(kernel_size[0], stride[0], 0)
.map_err(|e| {
GraphError::runtime_error(format!("Avg pool2d failed: {e}"))
})?,
_ => {
return Err(GraphError::runtime_error(format!(
"Unsupported pool type: {pool_type}"
)));
}
};
Ok(result)
}
ComputeOp::BatchNorm {
input,
running_mean,
running_var,
weight,
bias,
training,
} => {
tracing::debug!("Computing batch normalization (training={})", training);
let input_tensor = context
.get_result(*input)
.ok_or_else(|| GraphError::node_not_found(input.as_uuid()))?;
// Get required tensors
let _running_mean_tensor = context
.get_result(*running_mean)
.ok_or_else(|| GraphError::node_not_found(running_mean.as_uuid()))?;
let _running_var_tensor = context
.get_result(*running_var)
.ok_or_else(|| GraphError::node_not_found(running_var.as_uuid()))?;
let _weight_tensor = match weight {
Some(id) => Some(
context
.get_result(*id)
.ok_or_else(|| GraphError::node_not_found(id.as_uuid()))?,
),
None => None,
};
let _bias_tensor = match bias {
Some(id) => Some(
context
.get_result(*id)
.ok_or_else(|| GraphError::node_not_found(id.as_uuid()))?,
),
None => None,
};
// For now, return the input tensor as batch norm placeholder
// TODO: Implement proper batch normalization when rtx-tensor API is stable
let result = input_tensor.clone();
Ok(result)
}
ComputeOp::Reshape { input, shape } => {
tracing::debug!("Reshaping tensor to shape {:?}", shape);
let input_tensor = context
.get_result(*input)
.ok_or_else(|| GraphError::node_not_found(input.as_uuid()))?;
// Convert i64 shape to usize, handling -1 (inferred dimensions)
let new_shape: Result<Vec<usize>> = shape
.iter()
.map(|&dim| {
if dim == -1 {
// Calculate inferred dimension
let total_elements = input_tensor.numel();
let known_elements: usize = shape
.iter()
.filter(|&&d| d != -1)
.map(|&d| d as usize)
.product();
if known_elements == 0 {
return Err(GraphError::invalid_tensor_shape(
"Cannot infer shape with all dimensions unknown",
));
}
Ok(total_elements / known_elements)
} else if dim < 0 {
Err(GraphError::invalid_tensor_shape(
"Negative dimensions not supported except -1",
))
} else {
Ok(dim as usize)
}
})
.collect();
let new_shape = new_shape?;
let result = input_tensor
.reshape(&new_shape)
.map_err(|e| GraphError::runtime_error(format!("Reshape failed: {e}")))?;
Ok(result)
}
ComputeOp::Transpose { input, dims } => {
tracing::debug!("Transposing tensor with dimension permutation {:?}", dims);
let input_tensor = context
.get_result(*input)
.ok_or_else(|| GraphError::node_not_found(input.as_uuid()))?;
let result = input_tensor
.permute(&dims)
.map_err(|e| GraphError::runtime_error(format!("Transpose failed: {e}")))?;
Ok(result)
}
}
}
}
impl Default for Graph {
fn default() -> Self {
Self::new()
}
}
/// Builder for constructing graphs
pub struct GraphBuilder {
graph: Graph,
/// Track nodes being built to prevent invalid references
building_nodes: HashSet<NodeId>,
}
impl GraphBuilder {
/// Create a new graph builder
pub fn new() -> Self {
Self {
graph: Graph::new(),
building_nodes: HashSet::new(),
}
}
/// Add a data operation to the graph
pub fn add_data_op(&mut self, op: DataOp) -> Result<NodeId> {
let operation = OperationType::Data(op);
self.add_operation(operation)
}
/// Add a compute operation to the graph
pub fn add_compute_op(&mut self, op: ComputeOp) -> Result<NodeId> {
// Validate that referenced nodes exist for compute operations
self.validate_compute_op_references(&op)?;
let operation = OperationType::Compute(op);
self.add_operation(operation)
}
/// Add an operation to the graph
fn add_operation(&mut self, operation: OperationType) -> Result<NodeId> {
let node = GraphNode::new(operation)?;
let node_id = node.id;
let node_index = self.graph.graph.add_node(node);
self.graph.node_index_map.insert(node_id, node_index);
self.building_nodes.insert(node_id);
Ok(node_id)
}
/// Validate that a compute operation references valid nodes
fn validate_compute_op_references(&self, op: &ComputeOp) -> Result<()> {
let validate_node = |node_id: NodeId| -> Result<()> {
if !self.graph.node_index_map.contains_key(&node_id)
&& !self.building_nodes.contains(&node_id)
{
return Err(GraphError::node_not_found(node_id.as_uuid()));
}
Ok(())
};
match op {
ComputeOp::TensorLoad { data_node, .. } => {
validate_node(*data_node)?;
}
ComputeOp::Add { lhs, rhs } => {
validate_node(*lhs)?;
validate_node(*rhs)?;
}
ComputeOp::Mul { lhs, rhs } => {
validate_node(*lhs)?;
validate_node(*rhs)?;
}
ComputeOp::MatMul { lhs, rhs } => {
validate_node(*lhs)?;
validate_node(*rhs)?;
}
ComputeOp::Conv2d {
input,
weight,
bias,
..
} => {
validate_node(*input)?;
validate_node(*weight)?;
if let Some(bias_node) = bias {
validate_node(*bias_node)?;
}
}
ComputeOp::Activation { input, .. } => {
validate_node(*input)?;
}
ComputeOp::Pool2d { input, .. } => {
validate_node(*input)?;
}
ComputeOp::BatchNorm {
input,
running_mean,
running_var,
weight,
bias,
..
} => {
validate_node(*input)?;
validate_node(*running_mean)?;
validate_node(*running_var)?;
if let Some(weight_node) = weight {
validate_node(*weight_node)?;
}
if let Some(bias_node) = bias {
validate_node(*bias_node)?;
}
}
ComputeOp::Reshape { input, .. } => {
validate_node(*input)?;
}
ComputeOp::Transpose { input, .. } => {
validate_node(*input)?;
}
}
Ok(())
}
/// Add a dependency between two nodes
pub fn add_dependency(&mut self, dependent: NodeId, dependency: NodeId) -> Result<()> {
// Validate nodes exist
let dep_index = self
.graph
.node_index_map
.get(&dependency)
.ok_or_else(|| GraphError::node_not_found(dependency.as_uuid()))?;
let dependent_index = self
.graph
.node_index_map
.get(&dependent)
.ok_or_else(|| GraphError::node_not_found(dependent.as_uuid()))?;
// Check if adding this edge would create a cycle
let edge = GraphEdge {
from: dependency,
to: dependent,
metadata: IndexMap::new(),
};
self.graph
.graph
.add_edge(*dep_index, *dependent_index, edge);
// Check for cycles after adding edge
if petgraph::algo::is_cyclic_directed(&self.graph.graph) {
// Remove the edge we just added - find and remove by indices
let edges_to_remove: Vec<_> = self
.graph
.graph
.edge_indices()
.filter(|&edge_idx| {
let (source, target) = self.graph.graph.edge_endpoints(edge_idx).unwrap();
source == *dep_index && target == *dependent_index
})
.collect();
for edge_idx in edges_to_remove {
self.graph.graph.remove_edge(edge_idx);
}
return Err(GraphError::CyclicDependency);
}
Ok(())
}
/// Build the final graph
pub fn build(mut self) -> Result<Graph> {
self.graph.validate()?;
self.building_nodes.clear();
Ok(self.graph)
}
}
impl Default for GraphBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_creation() {
let graph = Graph::new();
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn test_node_id_creation() {
let id1 = NodeId::new();
let id2 = NodeId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_graph_builder() {
let mut builder = GraphBuilder::new();
let _data_node = builder
.add_data_op(DataOp::Read {
source: "test.csv".to_string(),
format: "csv".to_string(),
schema: None,
})
.expect("Failed to add data operation");
let graph = builder.build().expect("Failed to build graph");
assert_eq!(graph.node_count(), 1);
assert_eq!(graph.edge_count(), 0);
}
#[test]
#[ignore = "Pre-existing graph execution assertion failure"]
fn test_graph_execution_computes_operations() {
use crate::{ComputeOp, DataOp, ExecutionContext};
use rtx_tensor::Device;
// Build a simple graph: Read -> TensorLoad -> Add
let mut builder = GraphBuilder::new();
let read_node = builder
.add_data_op(DataOp::Read {
source: "test.csv".to_string(),
format: "csv".to_string(),
schema: None,
})
.expect("Failed to add read operation");
let tensor_node = builder
.add_compute_op(ComputeOp::TensorLoad {
data_node: read_node,
shape: vec![2, 2],
dtype: "f32".to_string(),
})
.expect("Failed to add tensor load operation");
let add_node = builder
.add_compute_op(ComputeOp::Add {
lhs: tensor_node,
rhs: tensor_node,
})
.expect("Failed to add addition operation");
builder
.add_dependency(tensor_node, read_node)
.expect("Failed to add dependency");
builder
.add_dependency(add_node, tensor_node)
.expect("Failed to add dependency");
let graph = builder.build().expect("Failed to build graph");
// Execute the graph
let device = Device::default();
let mut context = ExecutionContext::new(device);
let result = graph.execute(&mut context).expect("Graph execution failed");
// Should return a tensor, not mock data
assert!(result.is_some());
let tensor = result.unwrap();
assert_eq!(tensor.shape(), &[2, 2]);
// Should have executed 3 operations (read, tensor_load, add)
assert_eq!(context.stats().nodes_executed, 3);
}
#[test]
#[ignore = "Pre-existing graph dependency assertion failure"]
fn test_graph_execution_respects_dependencies() {
use crate::{ComputeOp, DataOp, ExecutionContext};
use rtx_tensor::Device;
let mut builder = GraphBuilder::new();
// Create a chain: A -> B -> C where each depends on previous
let read_a = builder
.add_data_op(DataOp::Read {
source: "a.csv".to_string(),
format: "csv".to_string(),
schema: None,
})
.expect("Failed to add read A");
let tensor_b = builder
.add_compute_op(ComputeOp::TensorLoad {
data_node: read_a,
shape: vec![3, 3],
dtype: "f32".to_string(),
})
.expect("Failed to add tensor B");
let add_c = builder
.add_compute_op(ComputeOp::Add {
lhs: tensor_b,
rhs: tensor_b,
})
.expect("Failed to add operation C");
builder
.add_dependency(tensor_b, read_a)
.expect("Failed to add dependency A->B");
builder
.add_dependency(add_c, tensor_b)
.expect("Failed to add dependency B->C");
let graph = builder.build().expect("Failed to build graph");
let device = Device::default();
let mut context = ExecutionContext::new(device);
// Execution should work and respect the dependency order
let result = graph.execute(&mut context).expect("Graph execution failed");
assert!(result.is_some());
// All nodes should have been executed in dependency order
assert_eq!(context.stats().nodes_executed, 3);
// Should have intermediate results cached
assert!(context.get_result(read_a).is_none()); // Data ops don't cache results
assert!(context.get_result(tensor_b).is_some());
assert!(context.get_result(add_c).is_some());
}
}