spatialrust_distribute/
graph.rs1use std::collections::{HashMap, HashSet, VecDeque};
4
5use crate::{DistributeError, DistributeResult};
6
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9pub struct ExecutionNode {
10 pub id: String,
12 pub device: String,
14}
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct ExecutionPartition {
19 pub id: String,
21 pub nodes: Vec<String>,
23}
24
25impl ExecutionPartition {
26 pub fn try_new(id: impl Into<String>, nodes: Vec<String>) -> DistributeResult<Self> {
28 let id = id.into();
29 if id.is_empty() {
30 return Err(DistributeError::InvalidConfiguration(
31 "partition id must be non-empty".into(),
32 ));
33 }
34 if nodes.is_empty() {
35 return Err(DistributeError::InvalidConfiguration(
36 "partition must contain at least one node".into(),
37 ));
38 }
39 if nodes.iter().any(|n| n.is_empty()) {
40 return Err(DistributeError::InvalidConfiguration("node ids must be non-empty".into()));
41 }
42 Ok(Self { id, nodes })
43 }
44
45 #[must_use]
47 pub fn contains(&self, node_id: &str) -> bool {
48 self.nodes.iter().any(|n| n == node_id)
49 }
50}
51
52#[derive(Clone, Debug, Default)]
54pub struct PartitionGraph {
55 partitions: HashMap<String, ExecutionPartition>,
56 edges: Vec<(String, String)>,
57}
58
59impl PartitionGraph {
60 #[must_use]
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn insert_partition(&mut self, partition: ExecutionPartition) -> DistributeResult<()> {
68 if partition.id.is_empty() {
69 return Err(DistributeError::InvalidConfiguration(
70 "partition id must be non-empty".into(),
71 ));
72 }
73 if partition.nodes.is_empty() {
74 return Err(DistributeError::InvalidConfiguration(
75 "partition must contain at least one node".into(),
76 ));
77 }
78 self.partitions.insert(partition.id.clone(), partition);
79 Ok(())
80 }
81
82 pub fn connect(
84 &mut self,
85 from: impl Into<String>,
86 to: impl Into<String>,
87 ) -> DistributeResult<()> {
88 let from = from.into();
89 let to = to.into();
90 if !self.partitions.contains_key(&from) || !self.partitions.contains_key(&to) {
91 return Err(DistributeError::Missing("partition endpoint".into()));
92 }
93 if from == to {
94 return Err(DistributeError::InvalidConfiguration("self-edges are not allowed".into()));
95 }
96 if self.edges.iter().any(|(a, b)| a == &from && b == &to) {
97 return Ok(());
98 }
99 self.edges.push((from, to));
100 Ok(())
101 }
102
103 #[must_use]
105 pub fn partitions(&self) -> &HashMap<String, ExecutionPartition> {
106 &self.partitions
107 }
108
109 #[must_use]
111 pub fn edges(&self) -> &[(String, String)] {
112 &self.edges
113 }
114
115 #[must_use]
117 pub fn partition(&self, id: &str) -> Option<&ExecutionPartition> {
118 self.partitions.get(id)
119 }
120
121 #[must_use]
123 pub fn partition_of_node(&self, node_id: &str) -> Option<&ExecutionPartition> {
124 self.partitions.values().find(|partition| partition.contains(node_id))
125 }
126
127 #[must_use]
129 pub fn successors(&self, id: &str) -> Vec<&str> {
130 self.edges.iter().filter_map(|(from, to)| (from == id).then_some(to.as_str())).collect()
131 }
132
133 pub fn topological_order(&self) -> DistributeResult<Vec<String>> {
135 let mut indegree: HashMap<&str, usize> =
136 self.partitions.keys().map(|id| (id.as_str(), 0usize)).collect();
137 for (from, to) in &self.edges {
138 if !indegree.contains_key(from.as_str()) || !indegree.contains_key(to.as_str()) {
139 return Err(DistributeError::Missing("partition endpoint".into()));
140 }
141 *indegree.get_mut(to.as_str()).unwrap() += 1;
142 }
143
144 let mut queue: VecDeque<String> = indegree
145 .iter()
146 .filter(|(_, deg)| **deg == 0)
147 .map(|(id, _)| (*id).to_string())
148 .collect();
149 let mut queued: HashSet<String> = queue.iter().cloned().collect();
151 let mut order = Vec::with_capacity(self.partitions.len());
152
153 while let Some(id) = {
154 let next = queue.iter().min().cloned();
156 if let Some(ref n) = next {
157 queue.retain(|x| x != n);
158 queued.remove(n);
159 }
160 next
161 } {
162 order.push(id.clone());
163 for succ in self.successors(&id) {
164 let deg = indegree.get_mut(succ).unwrap();
165 *deg = deg.saturating_sub(1);
166 if *deg == 0 && !queued.contains(succ) {
167 queued.insert(succ.to_string());
168 queue.push_back(succ.to_string());
169 }
170 }
171 }
172
173 if order.len() != self.partitions.len() {
174 return Err(DistributeError::CycleDetected);
175 }
176 Ok(order)
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{ExecutionPartition, PartitionGraph};
183
184 #[test]
185 fn connects_partitions_and_orders() {
186 let mut graph = PartitionGraph::new();
187 graph
188 .insert_partition(ExecutionPartition::try_new("edge", vec!["cam".into()]).unwrap())
189 .unwrap();
190 graph
191 .insert_partition(ExecutionPartition::try_new("cloud", vec!["infer".into()]).unwrap())
192 .unwrap();
193 graph.connect("edge", "cloud").unwrap();
194 assert_eq!(graph.edges().len(), 1);
195 assert_eq!(graph.topological_order().unwrap(), vec!["edge", "cloud"]);
196 assert_eq!(graph.partition_of_node("cam").unwrap().id, "edge");
197 }
198
199 #[test]
200 fn detects_cycle() {
201 let mut graph = PartitionGraph::new();
202 graph
203 .insert_partition(ExecutionPartition::try_new("a", vec!["n0".into()]).unwrap())
204 .unwrap();
205 graph
206 .insert_partition(ExecutionPartition::try_new("b", vec!["n1".into()]).unwrap())
207 .unwrap();
208 graph.connect("a", "b").unwrap();
209 graph.connect("b", "a").unwrap();
210 assert!(matches!(graph.topological_order(), Err(crate::DistributeError::CycleDetected)));
211 }
212}