1use std::collections::{BTreeMap, BTreeSet};
12
13use dspk_pipeline::Node;
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17use super::recipe::RecipeDoc;
18use super::session::{op_name, DisplayDoc, OpDoc, RecordingRef, ViewDoc};
19
20pub type Recipes = BTreeMap<String, RecipeDoc>;
22
23pub const COLUMN: f32 = 260.;
25pub const ROW: f32 = 150.;
26
27#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
28#[serde(deny_unknown_fields)]
29pub struct GraphDoc {
30 #[serde(default)]
31 pub nodes: Vec<GraphNode>,
32 #[serde(default)]
33 pub edges: Vec<EdgeDoc>,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
38#[serde(deny_unknown_fields)]
39pub struct GraphNode {
40 pub id: String,
42 #[serde(default)]
43 pub x: f32,
44 #[serde(default)]
45 pub y: f32,
46 pub kind: NodeKind,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
50#[serde(tag = "type", rename_all = "snake_case")]
51pub enum NodeKind {
52 Source { recording: RecordingRef },
54 Op {
56 #[serde(default = "yes")]
58 enabled: bool,
59 node: Node,
60 },
61 View {
63 name: String,
64 #[serde(default)]
65 display: DisplayDoc,
66 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
67 #[schemars(with = "BTreeMap<String, serde_json::Value>")]
68 plugins: BTreeMap<String, serde_json::Value>,
69 },
70 Write {},
72 Recipe {
74 recipe: String,
75 #[serde(default = "yes")]
76 enabled: bool,
77 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
79 #[schemars(with = "BTreeMap<String, serde_json::Value>")]
80 overrides: BTreeMap<String, serde_json::Value>,
81 },
82}
83
84fn yes() -> bool {
85 true
86}
87
88impl NodeKind {
89 pub fn has_input(&self) -> bool {
91 !matches!(self, NodeKind::Source { .. })
92 }
93
94 pub fn has_output(&self) -> bool {
96 matches!(self, NodeKind::Source { .. } | NodeKind::Op { .. } | NodeKind::Recipe { .. })
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
102#[serde(deny_unknown_fields)]
103pub struct EdgeDoc {
104 pub from: String,
105 pub to: String,
106}
107
108impl GraphDoc {
109 pub fn is_empty(&self) -> bool {
110 self.nodes.is_empty()
111 }
112
113 pub fn node(&self, id: &str) -> Option<&GraphNode> {
114 self.nodes.iter().find(|n| n.id == id)
115 }
116
117 pub fn node_mut(&mut self, id: &str) -> Option<&mut GraphNode> {
118 self.nodes.iter_mut().find(|n| n.id == id)
119 }
120
121 pub fn contains(&self, id: &str) -> bool {
122 self.node(id).is_some()
123 }
124
125 pub fn input(&self, id: &str) -> Option<&str> {
127 self.edges.iter().find(|e| e.to == id).map(|e| e.from.as_str())
128 }
129
130 pub fn outputs(&self, id: &str) -> Vec<&str> {
132 self.edges.iter().filter(|e| e.from == id).map(|e| e.to.as_str()).collect()
133 }
134
135 pub fn path(&self, id: &str) -> Option<Vec<String>> {
137 let mut path = vec![id.to_string()];
138 let mut cur = id;
139 for _ in 0..=self.nodes.len() {
142 if matches!(self.node(cur)?.kind, NodeKind::Source { .. }) {
143 path.reverse();
144 return Some(path);
145 }
146 cur = self.input(cur)?;
147 path.push(cur.to_string());
148 }
149 None
150 }
151
152 pub fn ancestors(&self, id: &str) -> BTreeSet<String> {
154 let mut out = BTreeSet::new();
155 let mut cur = id;
156 while let Some(i) = self.input(cur) {
157 if !out.insert(i.to_string()) {
158 break;
159 }
160 cur = i;
161 }
162 out
163 }
164
165 pub fn descendants(&self, id: &str) -> BTreeSet<String> {
167 let mut out = BTreeSet::new();
168 let mut stack = vec![id.to_string()];
169 while let Some(n) = stack.pop() {
170 for o in self.outputs(&n) {
171 if out.insert(o.to_string()) {
172 stack.push(o.to_string());
173 }
174 }
175 }
176 out
177 }
178
179 pub fn fresh_id(&self, base: &str) -> String {
181 if !self.contains(base) && base != "read" && base != "write" {
182 return base.to_string();
183 }
184 (1..).map(|i| format!("{base}{i}")).find(|id| !self.contains(id)).expect("ids are unbounded")
185 }
186
187 pub fn numbered_id(&self, prefix: &str, taken: impl Fn(&str) -> bool) -> String {
189 (1..).map(|i| format!("{prefix}{i}")).find(|id| !self.contains(id) && !taken(id)).expect("ids are unbounded")
190 }
191
192 pub fn next_row(&self) -> f32 {
194 self.nodes.iter().map(|n| n.y + ROW).fold(0.0, f32::max)
195 }
196
197 pub fn connect(&mut self, from: &str, to: &str) -> Result<(), String> {
199 let (Some(a), Some(b)) = (self.node(from), self.node(to)) else { return Err(format!("no node {from:?} or {to:?}")) };
200 if !a.kind.has_output() {
201 return Err(format!("{from} has no output (views and writes end a chain)"));
202 }
203 if !b.kind.has_input() {
204 return Err(format!("{to} is a recording: it takes no input"));
205 }
206 if from == to || self.descendants(to).contains(from) {
207 return Err("that connection would make a loop".into());
208 }
209 self.edges.retain(|e| e.to != to);
210 self.edges.push(EdgeDoc { from: from.to_string(), to: to.to_string() });
211 Ok(())
212 }
213
214 pub fn disconnect(&mut self, to: &str) {
216 self.edges.retain(|e| e.to != to);
217 }
218
219 pub fn remove(&mut self, id: &str) {
221 self.nodes.retain(|n| n.id != id);
222 self.edges.retain(|e| e.from != id && e.to != id);
223 }
224
225 pub fn remove_bridging(&mut self, id: &str) {
227 self.unlink_bridging(id);
228 self.nodes.retain(|n| n.id != id);
229 }
230
231 pub fn unlink_bridging(&mut self, id: &str) {
233 let input = self.input(id).map(str::to_string);
234 let outputs: Vec<String> = self.outputs(id).into_iter().map(str::to_string).collect();
235 self.edges.retain(|e| e.from != id && e.to != id);
236 if let Some(i) = input {
237 for o in outputs {
238 self.edges.push(EdgeDoc { from: i.clone(), to: o });
239 }
240 }
241 }
242
243 pub fn splice(&mut self, a: &str, b: &str, id: &str) {
245 self.edges.retain(|e| !(e.from == a && e.to == b));
246 self.edges.push(EdgeDoc { from: a.to_string(), to: id.to_string() });
247 self.edges.push(EdgeDoc { from: id.to_string(), to: b.to_string() });
248 }
249
250 pub fn chain(&self, id: &str, recipes: &Recipes) -> Option<(RecordingRef, Vec<OpDoc>)> {
253 let path = self.path(id)?;
254 let NodeKind::Source { recording } = &self.node(&path[0])?.kind else { return None };
255 let mut ops = Vec::new();
256 for n in &path[1..path.len() - 1] {
257 match &self.node(n)?.kind {
258 NodeKind::Op { enabled, node } => ops.push(OpDoc { id: n.clone(), enabled: *enabled, node: node.clone() }),
259 NodeKind::Recipe { recipe, enabled, overrides } => {
260 let def = recipes.get(recipe)?;
262 ops.extend(def.ops_with(overrides).into_iter().map(|op| OpDoc { id: format!("{n}.{}", op.id), enabled: op.enabled && *enabled, node: op.node }));
263 }
264 _ => {}
265 }
266 }
267 Some((recording.clone(), ops))
268 }
269
270 pub fn compile(&self, recipes: &Recipes) -> Vec<ViewDoc> {
272 self.nodes
273 .iter()
274 .filter_map(|n| {
275 let NodeKind::View { name, display, plugins } = &n.kind else { return None };
276 let (recording, ops) = self.chain(&n.id, recipes)?;
277 Some(ViewDoc { id: n.id.clone(), name: name.clone(), recording, ops, display: display.clone(), plugins: plugins.clone() })
278 })
279 .collect()
280 }
281
282 pub fn from_views(views: &[ViewDoc]) -> Self {
285 let mut g = GraphDoc::default();
286 let view_ids: BTreeSet<&str> = views.iter().map(|v| v.id.as_str()).collect();
287 for (row, v) in views.iter().enumerate() {
288 let y = row as f32 * ROW;
289 let s = g.numbered_id("s", |id| view_ids.contains(id));
290 g.nodes.push(GraphNode { id: s.clone(), x: 0.0, y, kind: NodeKind::Source { recording: v.recording.clone() } });
291 let mut last = s;
292 for (k, op) in v.ops.iter().enumerate() {
293 let id = if g.contains(&op.id) || view_ids.contains(op.id.as_str()) { g.fresh_id(op_name(&op.node)) } else { op.id.clone() };
294 g.nodes.push(GraphNode { id: id.clone(), x: (k + 1) as f32 * COLUMN, y, kind: NodeKind::Op { enabled: op.enabled, node: op.node.clone() } });
295 g.edges.push(EdgeDoc { from: last, to: id.clone() });
296 last = id;
297 }
298 let kind = NodeKind::View { name: v.name.clone(), display: v.display.clone(), plugins: v.plugins.clone() };
299 g.nodes.push(GraphNode { id: v.id.clone(), x: (v.ops.len() + 1) as f32 * COLUMN, y, kind });
300 g.edges.push(EdgeDoc { from: last, to: v.id.clone() });
301 }
302 g
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 fn node(id: &str, kind: NodeKind) -> GraphNode {
311 GraphNode { id: id.into(), x: 0.0, y: 0.0, kind }
312 }
313
314 fn view(name: &str) -> NodeKind {
315 NodeKind::View { name: name.into(), display: DisplayDoc::default(), plugins: BTreeMap::new() }
316 }
317
318 fn branched() -> GraphDoc {
320 let rec = RecordingRef { path: "/d/rec.bin".into(), source: None, channels: None };
321 let hp: Node = serde_json::from_value(serde_json::json!({"op": "iir", "band": "highpass", "cutoff": [300.0]})).unwrap();
322 let lp: Node = serde_json::from_value(serde_json::json!({"op": "iir", "band": "lowpass", "cutoff": [300.0]})).unwrap();
323 let mut g = GraphDoc::default();
324 g.nodes = vec![
325 node("s1", NodeKind::Source { recording: rec }),
326 node("hp", NodeKind::Op { enabled: true, node: hp }),
327 node("cmr", NodeKind::Op { enabled: true, node: Node::Cmr {} }),
328 node("spikes", view("Spikes")),
329 node("lp", NodeKind::Op { enabled: true, node: lp }),
330 node("lfp", view("LFP")),
331 ];
332 for (a, b) in [("s1", "hp"), ("hp", "cmr"), ("cmr", "spikes"), ("s1", "lp"), ("lp", "lfp")] {
333 g.connect(a, b).unwrap();
334 }
335 g
336 }
337
338 #[test]
339 fn test_branches_compile_to_one_view_each() {
340 let views = branched().compile(&Recipes::new());
341 assert_eq!(views.len(), 2);
342 assert_eq!(views[0].ops.iter().map(|o| o.id.as_str()).collect::<Vec<_>>(), ["hp", "cmr"]);
343 assert_eq!(views[1].ops.iter().map(|o| o.id.as_str()).collect::<Vec<_>>(), ["lp"]);
344 }
345
346 #[test]
347 fn test_paths_and_highlight_sets() {
348 let g = branched();
349 assert_eq!(g.path("spikes").unwrap(), ["s1", "hp", "cmr", "spikes"]);
350 assert_eq!(g.ancestors("cmr").into_iter().collect::<Vec<_>>(), ["hp", "s1"]);
351 assert_eq!(g.descendants("s1").len(), 5);
352 assert!(g.descendants("hp").contains("spikes") && !g.descendants("hp").contains("lfp"));
353 }
354
355 #[test]
356 fn test_connections_are_checked() {
357 let mut g = branched();
358 assert!(g.connect("spikes", "lp").is_err(), "a view has no output");
359 assert!(g.connect("cmr", "s1").is_err(), "a source has no input");
360 assert!(g.connect("cmr", "hp").is_err(), "a loop");
361 g.connect("cmr", "lfp").unwrap();
363 assert_eq!(g.input("lfp"), Some("cmr"));
364 assert_eq!(g.edges.iter().filter(|e| e.to == "lfp").count(), 1);
365 }
366
367 #[test]
368 fn test_removing_bridges_and_loose_views_drop_out() {
369 let mut g = branched();
370 g.remove_bridging("hp");
371 assert_eq!(g.path("spikes").unwrap(), ["s1", "cmr", "spikes"]);
372 g.disconnect("lfp");
373 assert_eq!(g.compile(&Recipes::new()).len(), 1, "a view without a source is not a view yet");
374 }
375
376 #[test]
377 fn test_from_views_round_trips() {
378 let views = branched().compile(&Recipes::new());
379 let again = GraphDoc::from_views(&views).compile(&Recipes::new());
380 assert_eq!(again.iter().map(|v| (&v.id, v.ops.len())).collect::<Vec<_>>(), views.iter().map(|v| (&v.id, v.ops.len())).collect::<Vec<_>>());
381 }
382}