1use std::cmp::Ordering;
4use std::collections::{BinaryHeap, HashMap, VecDeque};
5use std::sync::OnceLock;
6
7use runmat_builtins::{
8 Access, BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
9 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
10 ClassDef, MethodDef, ObjectInstance, PropertyDef, ResolveContext, StringArray, Tensor, Type,
11 Value,
12};
13use runmat_macros::runtime_builtin;
14
15use crate::builtins::common::tensor;
16use crate::builtins::table::{table_from_columns, table_variables};
17use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
18
19const GRAPH_CLASS: &str = "graph";
20const DIGRAPH_CLASS: &str = "digraph";
21const NUM_NODES_PROPERTY: &str = "NumNodes";
22const GRAPH_NAME: &str = "graph";
23const DIGRAPH_NAME: &str = "digraph";
24
25const INPUT_GRAPH: BuiltinParamDescriptor = BuiltinParamDescriptor {
26 name: "G",
27 ty: BuiltinParamType::Any,
28 arity: BuiltinParamArity::Required,
29 default: None,
30 description: "Graph or digraph object.",
31};
32const INPUT_VALUE: BuiltinParamDescriptor = BuiltinParamDescriptor {
33 name: "value",
34 ty: BuiltinParamType::Any,
35 arity: BuiltinParamArity::Required,
36 default: None,
37 description: "Input value.",
38};
39const INPUT_REST: BuiltinParamDescriptor = BuiltinParamDescriptor {
40 name: "args",
41 ty: BuiltinParamType::Any,
42 arity: BuiltinParamArity::Variadic,
43 default: None,
44 description: "Additional arguments.",
45};
46const OUTPUT_VALUE: BuiltinParamDescriptor = BuiltinParamDescriptor {
47 name: "value",
48 ty: BuiltinParamType::Any,
49 arity: BuiltinParamArity::Required,
50 default: None,
51 description: "Output value.",
52};
53const OUTPUTS_ONE: [BuiltinParamDescriptor; 1] = [OUTPUT_VALUE];
54const INPUTS_CONSTRUCTOR: [BuiltinParamDescriptor; 2] = [INPUT_VALUE, INPUT_REST];
55const INPUTS_GRAPH: [BuiltinParamDescriptor; 1] = [INPUT_GRAPH];
56const INPUTS_GRAPH_REST: [BuiltinParamDescriptor; 2] = [INPUT_GRAPH, INPUT_REST];
57
58const GRAPH_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
59 BuiltinSignatureDescriptor {
60 label: "G = graph(s, t)",
61 inputs: &INPUTS_CONSTRUCTOR,
62 outputs: &OUTPUTS_ONE,
63 },
64 BuiltinSignatureDescriptor {
65 label: "G = graph(s, t, weights, names)",
66 inputs: &INPUTS_CONSTRUCTOR,
67 outputs: &OUTPUTS_ONE,
68 },
69 BuiltinSignatureDescriptor {
70 label: "G = graph(A)",
71 inputs: &INPUTS_CONSTRUCTOR,
72 outputs: &OUTPUTS_ONE,
73 },
74];
75const DIGRAPH_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
76 BuiltinSignatureDescriptor {
77 label: "G = digraph(s, t)",
78 inputs: &INPUTS_CONSTRUCTOR,
79 outputs: &OUTPUTS_ONE,
80 },
81 BuiltinSignatureDescriptor {
82 label: "G = digraph(A)",
83 inputs: &INPUTS_CONSTRUCTOR,
84 outputs: &OUTPUTS_ONE,
85 },
86];
87const UNARY_GRAPH_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
88 label: "value = f(G)",
89 inputs: &INPUTS_GRAPH,
90 outputs: &OUTPUTS_ONE,
91}];
92const GRAPH_QUERY_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
93 label: "value = f(G, args)",
94 inputs: &INPUTS_GRAPH_REST,
95 outputs: &OUTPUTS_ONE,
96}];
97
98const ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
99 code: "RM.GRAPH.INVALID_ARGUMENT",
100 identifier: Some("RunMat:graph:InvalidArgument"),
101 when: "Graph arguments, node ids, node names, edge weights, or output counts are invalid.",
102 message: "graph: invalid argument",
103};
104const ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
105 code: "RM.GRAPH.INTERNAL",
106 identifier: Some("RunMat:graph:Internal"),
107 when: "RunMat cannot build the requested graph value or query output.",
108 message: "graph: internal error",
109};
110const GRAPH_ERRORS: [BuiltinErrorDescriptor; 2] = [ERROR_INVALID_ARGUMENT, ERROR_INTERNAL];
111
112pub const GRAPH_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
113 signatures: &GRAPH_SIGNATURES,
114 output_mode: BuiltinOutputMode::Fixed,
115 completion_policy: BuiltinCompletionPolicy::Public,
116 errors: &GRAPH_ERRORS,
117};
118pub const DIGRAPH_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
119 signatures: &DIGRAPH_SIGNATURES,
120 output_mode: BuiltinOutputMode::Fixed,
121 completion_policy: BuiltinCompletionPolicy::Public,
122 errors: &GRAPH_ERRORS,
123};
124pub const GRAPH_UNARY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
125 signatures: &UNARY_GRAPH_SIGNATURES,
126 output_mode: BuiltinOutputMode::Fixed,
127 completion_policy: BuiltinCompletionPolicy::Public,
128 errors: &GRAPH_ERRORS,
129};
130pub const GRAPH_QUERY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
131 signatures: &GRAPH_QUERY_SIGNATURES,
132 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
133 completion_policy: BuiltinCompletionPolicy::Public,
134 errors: &GRAPH_ERRORS,
135};
136
137#[derive(Clone, Debug)]
138struct GraphData {
139 directed: bool,
140 node_names: Option<Vec<String>>,
141 edges: Vec<Edge>,
142 node_count: usize,
143}
144
145#[derive(Clone, Debug)]
146struct Edge {
147 source: usize,
148 target: usize,
149 weight: f64,
150}
151
152#[derive(Clone, Copy, Debug)]
153struct QueueItem {
154 distance: f64,
155 node: usize,
156}
157
158impl Eq for QueueItem {}
159
160impl PartialEq for QueueItem {
161 fn eq(&self, other: &Self) -> bool {
162 self.node == other.node && self.distance.to_bits() == other.distance.to_bits()
163 }
164}
165
166impl Ord for QueueItem {
167 fn cmp(&self, other: &Self) -> Ordering {
168 other
169 .distance
170 .partial_cmp(&self.distance)
171 .unwrap_or(Ordering::Equal)
172 }
173}
174
175impl PartialOrd for QueueItem {
176 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
177 Some(self.cmp(other))
178 }
179}
180
181fn any_type(_args: &[Type], _ctx: &ResolveContext) -> Type {
182 Type::Unknown
183}
184
185fn numeric_type(_args: &[Type], _ctx: &ResolveContext) -> Type {
186 Type::tensor()
187}
188
189fn graph_error(name: &'static str, message: impl Into<String>) -> RuntimeError {
190 error_from_descriptor(name, ERROR_INVALID_ARGUMENT, message)
191}
192
193fn internal_error(name: &'static str, message: impl Into<String>) -> RuntimeError {
194 error_from_descriptor(name, ERROR_INTERNAL, message)
195}
196
197fn error_from_descriptor(
198 name: &'static str,
199 descriptor: BuiltinErrorDescriptor,
200 message: impl Into<String>,
201) -> RuntimeError {
202 let builder = build_runtime_error(message).with_builtin(name);
203 match descriptor.identifier {
204 Some(identifier) => builder.with_identifier(identifier).build(),
205 None => builder.build(),
206 }
207}
208
209async fn gather_values(values: Vec<Value>) -> BuiltinResult<Vec<Value>> {
210 let mut gathered = Vec::with_capacity(values.len());
211 for value in values {
212 gathered.push(gather_if_needed_async(&value).await?);
213 }
214 Ok(gathered)
215}
216
217#[runtime_builtin(
218 name = "graph",
219 category = "graph",
220 summary = "Create an undirected graph object.",
221 keywords = "graph,edges,nodes,network",
222 type_resolver(any_type),
223 descriptor(crate::builtins::graph::GRAPH_DESCRIPTOR),
224 builtin_path = "crate::builtins::graph"
225)]
226async fn graph_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
227 construct_graph(GRAPH_NAME, false, gather_values(args).await?)
228}
229
230#[runtime_builtin(
231 name = "digraph",
232 category = "graph",
233 summary = "Create a directed graph object.",
234 keywords = "digraph,directed graph,edges,nodes,network",
235 type_resolver(any_type),
236 descriptor(crate::builtins::graph::DIGRAPH_DESCRIPTOR),
237 builtin_path = "crate::builtins::graph"
238)]
239async fn digraph_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
240 construct_graph(DIGRAPH_NAME, true, gather_values(args).await?)
241}
242
243#[runtime_builtin(
244 name = "numnodes",
245 category = "graph",
246 summary = "Return the number of nodes in a graph.",
247 keywords = "numnodes,graph,nodes",
248 type_resolver(numeric_type),
249 descriptor(crate::builtins::graph::GRAPH_UNARY_DESCRIPTOR),
250 builtin_path = "crate::builtins::graph"
251)]
252async fn numnodes_builtin(graph: Value) -> BuiltinResult<Value> {
253 Ok(Value::Num(
254 graph_data(&graph, "numnodes")?.node_count as f64,
255 ))
256}
257
258#[runtime_builtin(
259 name = "numedges",
260 category = "graph",
261 summary = "Return the number of edges in a graph.",
262 keywords = "numedges,graph,edges",
263 type_resolver(numeric_type),
264 descriptor(crate::builtins::graph::GRAPH_UNARY_DESCRIPTOR),
265 builtin_path = "crate::builtins::graph"
266)]
267async fn numedges_builtin(graph: Value) -> BuiltinResult<Value> {
268 Ok(Value::Num(
269 graph_data(&graph, "numedges")?.edges.len() as f64
270 ))
271}
272
273#[runtime_builtin(
274 name = "adjacency",
275 category = "graph",
276 summary = "Return a dense adjacency matrix for a graph.",
277 keywords = "adjacency,graph,matrix",
278 type_resolver(numeric_type),
279 descriptor(crate::builtins::graph::GRAPH_UNARY_DESCRIPTOR),
280 builtin_path = "crate::builtins::graph"
281)]
282async fn adjacency_builtin(graph: Value) -> BuiltinResult<Value> {
283 let graph = graph_data(&graph, "adjacency")?;
284 Ok(Value::Tensor(adjacency_tensor(&graph, "adjacency")?))
285}
286
287#[runtime_builtin(
288 name = "degree",
289 category = "graph",
290 summary = "Return graph node degree counts.",
291 keywords = "degree,graph,nodes",
292 type_resolver(numeric_type),
293 descriptor(crate::builtins::graph::GRAPH_UNARY_DESCRIPTOR),
294 builtin_path = "crate::builtins::graph"
295)]
296async fn degree_builtin(graph: Value) -> BuiltinResult<Value> {
297 let graph = graph_data(&graph, "degree")?;
298 node_metric_value(degree_counts(&graph), "degree")
299}
300
301#[runtime_builtin(
302 name = "indegree",
303 category = "graph",
304 summary = "Return node indegree counts.",
305 keywords = "indegree,digraph,graph,nodes",
306 type_resolver(numeric_type),
307 descriptor(crate::builtins::graph::GRAPH_UNARY_DESCRIPTOR),
308 builtin_path = "crate::builtins::graph"
309)]
310async fn indegree_builtin(graph: Value) -> BuiltinResult<Value> {
311 let graph = graph_data(&graph, "indegree")?;
312 node_metric_value(indegree_counts(&graph), "indegree")
313}
314
315#[runtime_builtin(
316 name = "outdegree",
317 category = "graph",
318 summary = "Return node outdegree counts.",
319 keywords = "outdegree,digraph,graph,nodes",
320 type_resolver(numeric_type),
321 descriptor(crate::builtins::graph::GRAPH_UNARY_DESCRIPTOR),
322 builtin_path = "crate::builtins::graph"
323)]
324async fn outdegree_builtin(graph: Value) -> BuiltinResult<Value> {
325 let graph = graph_data(&graph, "outdegree")?;
326 node_metric_value(outdegree_counts(&graph), "outdegree")
327}
328
329#[runtime_builtin(
330 name = "neighbors",
331 category = "graph",
332 summary = "Return neighboring graph nodes.",
333 keywords = "neighbors,graph,nodes",
334 type_resolver(any_type),
335 descriptor(crate::builtins::graph::GRAPH_QUERY_DESCRIPTOR),
336 builtin_path = "crate::builtins::graph"
337)]
338async fn neighbors_builtin(graph: Value, node: Value) -> BuiltinResult<Value> {
339 neighbor_query(graph, node, NeighborMode::Undirected, "neighbors").await
340}
341
342#[runtime_builtin(
343 name = "successors",
344 category = "graph",
345 summary = "Return successor nodes in a directed graph.",
346 keywords = "successors,digraph,graph,nodes",
347 type_resolver(any_type),
348 descriptor(crate::builtins::graph::GRAPH_QUERY_DESCRIPTOR),
349 builtin_path = "crate::builtins::graph"
350)]
351async fn successors_builtin(graph: Value, node: Value) -> BuiltinResult<Value> {
352 neighbor_query(graph, node, NeighborMode::Successors, "successors").await
353}
354
355#[runtime_builtin(
356 name = "predecessors",
357 category = "graph",
358 summary = "Return predecessor nodes in a directed graph.",
359 keywords = "predecessors,digraph,graph,nodes",
360 type_resolver(any_type),
361 descriptor(crate::builtins::graph::GRAPH_QUERY_DESCRIPTOR),
362 builtin_path = "crate::builtins::graph"
363)]
364async fn predecessors_builtin(graph: Value, node: Value) -> BuiltinResult<Value> {
365 neighbor_query(graph, node, NeighborMode::Predecessors, "predecessors").await
366}
367
368#[runtime_builtin(
369 name = "findedge",
370 category = "graph",
371 summary = "Find edge indices between graph nodes.",
372 keywords = "findedge,graph,edges",
373 type_resolver(numeric_type),
374 descriptor(crate::builtins::graph::GRAPH_QUERY_DESCRIPTOR),
375 builtin_path = "crate::builtins::graph"
376)]
377async fn findedge_builtin(graph: Value, source: Value, target: Value) -> BuiltinResult<Value> {
378 let graph = graph_data(&graph, "findedge")?;
379 let source = node_index_from_value(&graph, &source, "findedge")?;
380 let target = node_index_from_value(&graph, &target, "findedge")?;
381 for (idx, edge) in graph.edges.iter().enumerate() {
382 if edge_matches(edge, source, target, graph.directed) {
383 return Ok(Value::Num((idx + 1) as f64));
384 }
385 }
386 Ok(Value::Num(0.0))
387}
388
389#[runtime_builtin(
390 name = "shortestpath",
391 category = "graph",
392 summary = "Compute a shortest path between graph nodes.",
393 keywords = "shortestpath,graph,path,dijkstra",
394 type_resolver(any_type),
395 descriptor(crate::builtins::graph::GRAPH_QUERY_DESCRIPTOR),
396 builtin_path = "crate::builtins::graph"
397)]
398async fn shortestpath_builtin(graph: Value, source: Value, target: Value) -> BuiltinResult<Value> {
399 let graph = graph_data(&graph, "shortestpath")?;
400 let source = node_index_from_value(&graph, &source, "shortestpath")?;
401 let target = node_index_from_value(&graph, &target, "shortestpath")?;
402 let (distances, previous) = dijkstra(&graph, source);
403 let path = reconstruct_path(source, target, &previous);
404 let path_value = node_list_value(&graph, &path, "shortestpath")?;
405 let distance = distances[target];
406 match crate::output_count::current_output_count() {
407 Some(0) => Ok(Value::OutputList(Vec::new())),
408 Some(1) => Ok(Value::OutputList(vec![path_value])),
409 Some(2) => Ok(Value::OutputList(vec![path_value, Value::Num(distance)])),
410 Some(_) => Err(graph_error(
411 "shortestpath",
412 "shortestpath: too many output arguments; maximum is 2",
413 )),
414 None => Ok(path_value),
415 }
416}
417
418#[runtime_builtin(
419 name = "distances",
420 category = "graph",
421 summary = "Compute shortest-path distances between graph nodes.",
422 keywords = "distances,graph,dijkstra",
423 type_resolver(numeric_type),
424 descriptor(crate::builtins::graph::GRAPH_QUERY_DESCRIPTOR),
425 builtin_path = "crate::builtins::graph"
426)]
427async fn distances_builtin(graph: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
428 let graph = graph_data(&graph, "distances")?;
429 let rest = gather_values(rest).await?;
430 let sources = match rest.first() {
431 Some(value) => node_indices_from_value(&graph, value, "distances")?,
432 None => (0..graph.node_count).collect(),
433 };
434 let targets = match rest.get(1) {
435 Some(value) => node_indices_from_value(&graph, value, "distances")?,
436 None => (0..graph.node_count).collect(),
437 };
438 let mut data = vec![0.0; sources.len() * targets.len()];
439 for (source_row, source) in sources.iter().copied().enumerate() {
440 let (distances, _) = dijkstra(&graph, source);
441 for (target_col, target) in targets.iter().copied().enumerate() {
442 data[source_row + target_col * sources.len()] = distances[target];
443 }
444 }
445 tensor_value(data, vec![sources.len(), targets.len()], "distances")
446}
447
448#[runtime_builtin(
449 name = "conncomp",
450 category = "graph",
451 summary = "Return weak connected components for graph nodes.",
452 keywords = "conncomp,graph,components",
453 type_resolver(numeric_type),
454 descriptor(crate::builtins::graph::GRAPH_UNARY_DESCRIPTOR),
455 builtin_path = "crate::builtins::graph"
456)]
457async fn conncomp_builtin(graph: Value) -> BuiltinResult<Value> {
458 let graph = graph_data(&graph, "conncomp")?;
459 let mut bins = vec![0.0; graph.node_count];
460 let mut component = 0usize;
461 for start in 0..graph.node_count {
462 if bins[start] != 0.0 {
463 continue;
464 }
465 component += 1;
466 let mut queue = VecDeque::from([start]);
467 bins[start] = component as f64;
468 while let Some(node) = queue.pop_front() {
469 for next in neighbors_for(&graph, node, NeighborMode::Undirected) {
470 if bins[next] == 0.0 {
471 bins[next] = component as f64;
472 queue.push_back(next);
473 }
474 }
475 }
476 }
477 tensor_value(bins, vec![1, graph.node_count], "conncomp")
478}
479
480#[runtime_builtin(
481 name = "toposort",
482 category = "graph",
483 summary = "Return a topological ordering of a directed acyclic graph.",
484 keywords = "toposort,digraph,directed acyclic graph",
485 type_resolver(any_type),
486 descriptor(crate::builtins::graph::GRAPH_UNARY_DESCRIPTOR),
487 builtin_path = "crate::builtins::graph"
488)]
489async fn toposort_builtin(graph: Value) -> BuiltinResult<Value> {
490 let graph = graph_data(&graph, "toposort")?;
491 if !graph.directed {
492 return Err(graph_error(
493 "toposort",
494 "toposort: expected a digraph input",
495 ));
496 }
497 let mut indegree = vec![0usize; graph.node_count];
498 let adj = adjacency_lists(&graph, NeighborMode::Successors);
499 for edge in &graph.edges {
500 indegree[edge.target] += 1;
501 }
502 let mut queue = VecDeque::new();
503 for (idx, degree) in indegree.iter().copied().enumerate() {
504 if degree == 0 {
505 queue.push_back(idx);
506 }
507 }
508 let mut order = Vec::with_capacity(graph.node_count);
509 while let Some(node) = queue.pop_front() {
510 order.push(node);
511 for &next in &adj[node] {
512 indegree[next] -= 1;
513 if indegree[next] == 0 {
514 queue.push_back(next);
515 }
516 }
517 }
518 if order.len() != graph.node_count {
519 return Err(graph_error("toposort", "toposort: graph contains a cycle"));
520 }
521 node_list_value(&graph, &order, "toposort")
522}
523
524#[runtime_builtin(
525 name = "bfsearch",
526 category = "graph",
527 summary = "Return breadth-first search node order.",
528 keywords = "bfsearch,graph,breadth first search",
529 type_resolver(any_type),
530 descriptor(crate::builtins::graph::GRAPH_QUERY_DESCRIPTOR),
531 builtin_path = "crate::builtins::graph"
532)]
533async fn bfsearch_builtin(graph: Value, start: Value) -> BuiltinResult<Value> {
534 let graph = graph_data(&graph, "bfsearch")?;
535 let start = node_index_from_value(&graph, &start, "bfsearch")?;
536 let mut visited = vec![false; graph.node_count];
537 let mut queue = VecDeque::from([start]);
538 let mut order = Vec::new();
539 visited[start] = true;
540 while let Some(node) = queue.pop_front() {
541 order.push(node);
542 for next in neighbors_for(&graph, node, NeighborMode::Successors) {
543 if !visited[next] {
544 visited[next] = true;
545 queue.push_back(next);
546 }
547 }
548 }
549 node_list_value(&graph, &order, "bfsearch")
550}
551
552#[runtime_builtin(
553 name = "dfsearch",
554 category = "graph",
555 summary = "Return depth-first search node order.",
556 keywords = "dfsearch,graph,depth first search",
557 type_resolver(any_type),
558 descriptor(crate::builtins::graph::GRAPH_QUERY_DESCRIPTOR),
559 builtin_path = "crate::builtins::graph"
560)]
561async fn dfsearch_builtin(graph: Value, start: Value) -> BuiltinResult<Value> {
562 let graph = graph_data(&graph, "dfsearch")?;
563 let start = node_index_from_value(&graph, &start, "dfsearch")?;
564 let adj = adjacency_lists(&graph, NeighborMode::Successors);
565 let mut visited = vec![false; graph.node_count];
566 let mut stack = vec![start];
567 let mut order = Vec::new();
568 while let Some(node) = stack.pop() {
569 if visited[node] {
570 continue;
571 }
572 visited[node] = true;
573 order.push(node);
574 for &next in adj[node].iter().rev() {
575 if !visited[next] {
576 stack.push(next);
577 }
578 }
579 }
580 node_list_value(&graph, &order, "dfsearch")
581}
582
583#[runtime_builtin(
584 name = "treelayout",
585 category = "graph",
586 summary = "Compute simple tree-layout coordinates from a parent vector.",
587 keywords = "treelayout,tree,layout,graph",
588 type_resolver(numeric_type),
589 descriptor(crate::builtins::graph::GRAPH_QUERY_DESCRIPTOR),
590 builtin_path = "crate::builtins::graph"
591)]
592async fn treelayout_builtin(parents: Value) -> BuiltinResult<Value> {
593 let parents = gather_if_needed_async(&parents).await?;
594 let parents = numeric_vector(&parents, "treelayout")?;
595 if parents.is_empty() {
596 return Ok(Value::OutputList(vec![
597 tensor_value(Vec::new(), vec![1, 0], "treelayout")?,
598 tensor_value(Vec::new(), vec![1, 0], "treelayout")?,
599 ]));
600 }
601 let n = parents.len();
602 let mut children = vec![Vec::<usize>::new(); n];
603 let mut roots = Vec::new();
604 for (idx, parent) in parents.iter().copied().enumerate() {
605 if parent == 0 {
606 roots.push(idx);
607 } else if parent <= n {
608 children[parent - 1].push(idx);
609 } else {
610 return Err(graph_error(
611 "treelayout",
612 "treelayout: parent indices must be zero or valid one-based node ids",
613 ));
614 }
615 }
616 let mut x = vec![0.0; n];
617 let mut y = vec![0.0; n];
618 let mut leaf_cursor = 1.0;
619 for root in roots {
620 assign_tree_positions(root, 0.0, &children, &mut leaf_cursor, &mut x, &mut y);
621 }
622 let x_value = tensor_value(x, vec![1, n], "treelayout")?;
623 let y_value = tensor_value(y, vec![1, n], "treelayout")?;
624 match crate::output_count::current_output_count() {
625 Some(0) => Ok(Value::OutputList(Vec::new())),
626 Some(1) => Ok(Value::OutputList(vec![x_value])),
627 Some(2) => Ok(Value::OutputList(vec![x_value, y_value])),
628 Some(_) => Err(graph_error(
629 "treelayout",
630 "treelayout: too many output arguments; maximum is 2",
631 )),
632 None => Ok(x_value),
633 }
634}
635
636fn construct_graph(name: &'static str, directed: bool, args: Vec<Value>) -> BuiltinResult<Value> {
637 if args.is_empty() {
638 return graph_object(GraphData {
639 directed,
640 node_names: None,
641 edges: Vec::new(),
642 node_count: 0,
643 });
644 }
645 if args.len() == 1 || is_adjacency_form(&args) {
646 return graph_from_adjacency(name, directed, &args);
647 }
648 graph_from_edges(name, directed, &args)
649}
650
651fn is_adjacency_form(args: &[Value]) -> bool {
652 matches!(args.first(), Some(Value::Tensor(t)) if t.rows == t.cols)
653 && (args.len() == 1
654 || args
655 .get(1)
656 .is_some_and(|arg| parse_string_vector(arg).is_ok() || scalar_text(arg).is_some()))
657}
658
659fn graph_from_adjacency(
660 name: &'static str,
661 directed: bool,
662 args: &[Value],
663) -> BuiltinResult<Value> {
664 let Value::Tensor(matrix) = &args[0] else {
665 return Err(graph_error(
666 name,
667 format!("{name}: expected adjacency matrix"),
668 ));
669 };
670 if matrix.rows != matrix.cols {
671 return Err(graph_error(
672 name,
673 format!("{name}: adjacency matrix must be square"),
674 ));
675 }
676 let node_count = matrix.rows;
677 let mut mode = TriangleMode::Full;
678 let mut node_names = None;
679 if let Some(arg) = args.get(1) {
680 if let Some(text) = scalar_text(arg) {
681 match canonical(&text).as_str() {
682 "upper" => mode = TriangleMode::Upper,
683 "lower" => mode = TriangleMode::Lower,
684 _ => node_names = Some(parse_node_names(arg, node_count, name)?),
685 }
686 } else {
687 node_names = Some(parse_node_names(arg, node_count, name)?);
688 }
689 }
690 let mut edges = Vec::new();
691 for col in 0..node_count {
692 for row in 0..node_count {
693 let weight = if directed {
694 matrix.data[row + col * matrix.rows]
695 } else {
696 match mode {
697 TriangleMode::Full => {
698 if row > col {
699 continue;
700 }
701 let upper = matrix.data[row + col * matrix.rows];
702 if upper != 0.0 {
703 upper
704 } else if row == col {
705 0.0
706 } else {
707 matrix.data[col + row * matrix.rows]
708 }
709 }
710 TriangleMode::Upper => {
711 if row > col {
712 continue;
713 }
714 matrix.data[row + col * matrix.rows]
715 }
716 TriangleMode::Lower => {
717 if row < col {
718 continue;
719 }
720 matrix.data[row + col * matrix.rows]
721 }
722 }
723 };
724 if weight != 0.0 {
725 edges.push(Edge {
726 source: row,
727 target: col,
728 weight,
729 });
730 }
731 }
732 }
733 graph_object(GraphData {
734 directed,
735 node_names,
736 edges,
737 node_count,
738 })
739}
740
741fn graph_from_edges(name: &'static str, directed: bool, args: &[Value]) -> BuiltinResult<Value> {
742 if args.len() < 2 {
743 return Err(graph_error(
744 name,
745 format!("{name}: expected source and target node lists"),
746 ));
747 }
748 let source_nodes = node_tokens(&args[0], name)?;
749 let target_nodes = node_tokens(&args[1], name)?;
750 if source_nodes.len() != target_nodes.len() {
751 return Err(graph_error(
752 name,
753 format!("{name}: source and target lists must have the same length"),
754 ));
755 }
756 let weights = match args.get(2) {
757 Some(value) if looks_like_weights(value, source_nodes.len()) => {
758 weights_from_value(value, source_nodes.len(), name)?
759 }
760 _ => vec![1.0; source_nodes.len()],
761 };
762 let names_arg = if args.len() >= 4 {
763 args.get(3)
764 } else if args.len() == 3 && !looks_like_weights(&args[2], source_nodes.len()) {
765 args.get(2)
766 } else {
767 None
768 };
769
770 let mut node_names = names_arg
771 .map(|value| parse_string_vector(value))
772 .transpose()
773 .map_err(|err| graph_error(name, format!("{name}: {err}")))?;
774 let mut node_count = node_names.as_ref().map_or(0, Vec::len);
775 let mut label_to_index = HashMap::<String, usize>::new();
776 if let Some(names) = &node_names {
777 for (idx, node_name) in names.iter().enumerate() {
778 label_to_index.insert(node_name.clone(), idx);
779 }
780 }
781
782 let mut edges = Vec::with_capacity(source_nodes.len());
783 for ((source, target), weight) in source_nodes.into_iter().zip(target_nodes).zip(weights) {
784 let source = resolve_node_token(
785 source,
786 &mut node_names,
787 &mut label_to_index,
788 &mut node_count,
789 name,
790 )?;
791 let target = resolve_node_token(
792 target,
793 &mut node_names,
794 &mut label_to_index,
795 &mut node_count,
796 name,
797 )?;
798 edges.push(Edge {
799 source,
800 target,
801 weight,
802 });
803 }
804
805 graph_object(GraphData {
806 directed,
807 node_names,
808 edges,
809 node_count,
810 })
811}
812
813fn graph_object(graph: GraphData) -> BuiltinResult<Value> {
814 ensure_graph_classes_registered();
815 let class_name = if graph.directed {
816 DIGRAPH_CLASS
817 } else {
818 GRAPH_CLASS
819 };
820 let mut object = ObjectInstance::new(class_name.to_string());
821 object.properties.insert(
822 NUM_NODES_PROPERTY.to_string(),
823 Value::Num(graph.node_count as f64),
824 );
825 object
826 .properties
827 .insert("Edges".to_string(), edges_table(&graph)?);
828 object
829 .properties
830 .insert("Nodes".to_string(), nodes_table(&graph)?);
831 Ok(Value::Object(object))
832}
833
834fn edges_table(graph: &GraphData) -> BuiltinResult<Value> {
835 let names = graph.node_names.as_ref();
836 let end_nodes = if let Some(node_names) = names {
837 let mut values = Vec::with_capacity(graph.edges.len() * 2);
838 values.extend(
839 graph
840 .edges
841 .iter()
842 .map(|edge| node_names[edge.source].clone()),
843 );
844 values.extend(
845 graph
846 .edges
847 .iter()
848 .map(|edge| node_names[edge.target].clone()),
849 );
850 Value::StringArray(
851 StringArray::new(values, vec![graph.edges.len(), 2])
852 .map_err(|err| internal_error("graph", err))?,
853 )
854 } else {
855 let mut values = Vec::with_capacity(graph.edges.len() * 2);
856 values.extend(graph.edges.iter().map(|edge| (edge.source + 1) as f64));
857 values.extend(graph.edges.iter().map(|edge| (edge.target + 1) as f64));
858 tensor_value_raw(values, vec![graph.edges.len(), 2], "graph")?
859 };
860 let all_unit = graph.edges.iter().all(|edge| edge.weight == 1.0);
861 if all_unit {
862 table_from_columns(vec!["EndNodes".to_string()], vec![end_nodes])
863 } else {
864 let weights: Vec<f64> = graph.edges.iter().map(|edge| edge.weight).collect();
865 table_from_columns(
866 vec!["EndNodes".to_string(), "Weight".to_string()],
867 vec![
868 end_nodes,
869 tensor_value_raw(weights, vec![graph.edges.len(), 1], "graph")?,
870 ],
871 )
872 }
873}
874
875fn nodes_table(graph: &GraphData) -> BuiltinResult<Value> {
876 if let Some(names) = &graph.node_names {
877 table_from_columns(
878 vec!["Name".to_string()],
879 vec![Value::StringArray(
880 StringArray::new(names.clone(), vec![graph.node_count, 1])
881 .map_err(|err| internal_error("graph", err))?,
882 )],
883 )
884 } else {
885 table_from_columns(
886 vec!["Index".to_string()],
887 vec![tensor_value_raw(
888 (1..=graph.node_count).map(|idx| idx as f64).collect(),
889 vec![graph.node_count, 1],
890 "graph",
891 )?],
892 )
893 }
894}
895
896fn graph_data(value: &Value, name: &'static str) -> BuiltinResult<GraphData> {
897 let object = match value {
898 Value::Object(object)
899 if object.class_name == GRAPH_CLASS || object.class_name == DIGRAPH_CLASS =>
900 {
901 object
902 }
903 other => {
904 return Err(graph_error(
905 name,
906 format!("{name}: expected graph or digraph object, got {other:?}"),
907 ))
908 }
909 };
910 let directed = object.class_name == DIGRAPH_CLASS;
911 let node_count = match object.properties.get(NUM_NODES_PROPERTY) {
912 Some(value) => positive_integer_scalar(value, name)?,
913 None => infer_node_count_from_nodes(object, name)?,
914 };
915 let node_names = node_names_from_object(object, node_count, name)?;
916 let edges = edges_from_object(object, node_names.as_deref(), directed, node_count, name)?;
917 Ok(GraphData {
918 directed,
919 node_names,
920 edges,
921 node_count,
922 })
923}
924
925fn node_names_from_object(
926 object: &ObjectInstance,
927 node_count: usize,
928 name: &'static str,
929) -> BuiltinResult<Option<Vec<String>>> {
930 let Some(Value::Object(nodes)) = object.properties.get("Nodes") else {
931 return Ok(None);
932 };
933 let variables = table_variables(nodes)
934 .map_err(|err| graph_error(name, format!("{name}: invalid Nodes table: {err}")))?;
935 let Some(value) = variables.fields.get("Name") else {
936 return Ok(None);
937 };
938 let names = parse_string_vector(value).map_err(|err| graph_error(name, err))?;
939 if names.len() != node_count {
940 return Err(graph_error(
941 name,
942 format!("{name}: Nodes.Name length does not match graph node count"),
943 ));
944 }
945 Ok(Some(names))
946}
947
948fn infer_node_count_from_nodes(
949 object: &ObjectInstance,
950 name: &'static str,
951) -> BuiltinResult<usize> {
952 let Some(Value::Object(nodes)) = object.properties.get("Nodes") else {
953 return Ok(0);
954 };
955 crate::builtins::table::table_height(nodes)
956 .map_err(|err| graph_error(name, format!("{name}: invalid Nodes table: {err}")))
957}
958
959fn edges_from_object(
960 object: &ObjectInstance,
961 node_names: Option<&[String]>,
962 directed: bool,
963 node_count: usize,
964 name: &'static str,
965) -> BuiltinResult<Vec<Edge>> {
966 let Some(Value::Object(edges_table)) = object.properties.get("Edges") else {
967 return Ok(Vec::new());
968 };
969 let variables = table_variables(edges_table)
970 .map_err(|err| graph_error(name, format!("{name}: invalid Edges table: {err}")))?;
971 let Some(endnodes) = variables.fields.get("EndNodes") else {
972 return Ok(Vec::new());
973 };
974 let endpoint_pairs = edge_pairs_from_value(endnodes, node_names, node_count, name)?;
975 let weights = match variables.fields.get("Weight") {
976 Some(value) => weights_from_value(value, endpoint_pairs.len(), name)?,
977 None => vec![1.0; endpoint_pairs.len()],
978 };
979 let mut edges = Vec::with_capacity(endpoint_pairs.len());
980 for ((source, target), weight) in endpoint_pairs.into_iter().zip(weights) {
981 if source >= node_count || target >= node_count {
982 return Err(graph_error(
983 name,
984 format!("{name}: edge endpoint exceeds graph node count"),
985 ));
986 }
987 edges.push(Edge {
988 source,
989 target,
990 weight,
991 });
992 }
993 if !directed {
994 for edge in &edges {
995 if edge.source >= node_count || edge.target >= node_count {
996 return Err(graph_error(name, format!("{name}: invalid edge endpoint")));
997 }
998 }
999 }
1000 Ok(edges)
1001}
1002
1003fn edge_pairs_from_value(
1004 value: &Value,
1005 node_names: Option<&[String]>,
1006 node_count: usize,
1007 name: &'static str,
1008) -> BuiltinResult<Vec<(usize, usize)>> {
1009 match value {
1010 Value::Tensor(tensor) if tensor.cols == 2 => {
1011 let mut out = Vec::with_capacity(tensor.rows);
1012 for row in 0..tensor.rows {
1013 let source = node_index_from_f64(tensor.data[row], node_count, name)?;
1014 let target = node_index_from_f64(tensor.data[row + tensor.rows], node_count, name)?;
1015 out.push((source, target));
1016 }
1017 Ok(out)
1018 }
1019 Value::StringArray(array) if array.cols == 2 => {
1020 let Some(names) = node_names else {
1021 return Err(graph_error(
1022 name,
1023 format!("{name}: string EndNodes require Nodes.Name values"),
1024 ));
1025 };
1026 let lookup: HashMap<&str, usize> = names
1027 .iter()
1028 .enumerate()
1029 .map(|(idx, text)| (text.as_str(), idx))
1030 .collect();
1031 let mut out = Vec::with_capacity(array.rows);
1032 for row in 0..array.rows {
1033 let source = *lookup.get(array.data[row].as_str()).ok_or_else(|| {
1034 graph_error(
1035 name,
1036 format!("{name}: unknown source node '{}'", array.data[row]),
1037 )
1038 })?;
1039 let target = *lookup
1040 .get(array.data[row + array.rows].as_str())
1041 .ok_or_else(|| {
1042 graph_error(
1043 name,
1044 format!(
1045 "{name}: unknown target node '{}'",
1046 array.data[row + array.rows]
1047 ),
1048 )
1049 })?;
1050 out.push((source, target));
1051 }
1052 Ok(out)
1053 }
1054 other => Err(graph_error(
1055 name,
1056 format!(
1057 "{name}: Edges.EndNodes must be an m-by-2 numeric or string array, got {other:?}"
1058 ),
1059 )),
1060 }
1061}
1062
1063fn node_tokens(value: &Value, name: &'static str) -> BuiltinResult<Vec<NodeToken>> {
1064 if let Ok(values) = numeric_vector(value, name) {
1065 return Ok(values.into_iter().map(NodeToken::Index).collect());
1066 }
1067 Ok(parse_string_vector(value)
1068 .map_err(|err| graph_error(name, format!("{name}: {err}")))?
1069 .into_iter()
1070 .map(NodeToken::Name)
1071 .collect())
1072}
1073
1074#[derive(Debug)]
1075enum NodeToken {
1076 Index(usize),
1077 Name(String),
1078}
1079
1080fn resolve_node_token(
1081 token: NodeToken,
1082 node_names: &mut Option<Vec<String>>,
1083 label_to_index: &mut HashMap<String, usize>,
1084 node_count: &mut usize,
1085 name: &'static str,
1086) -> BuiltinResult<usize> {
1087 match token {
1088 NodeToken::Index(index) => {
1089 if index == 0 {
1090 return Err(graph_error(
1091 name,
1092 format!("{name}: node indices are one-based"),
1093 ));
1094 }
1095 let zero = index - 1;
1096 *node_count = (*node_count).max(index);
1097 if let Some(names) = node_names {
1098 if zero >= names.len() {
1099 return Err(graph_error(
1100 name,
1101 format!("{name}: numeric node index exceeds supplied node names"),
1102 ));
1103 }
1104 }
1105 Ok(zero)
1106 }
1107 NodeToken::Name(label) => {
1108 if node_names.is_none() {
1109 *node_names = Some(Vec::new());
1110 }
1111 if let Some(&idx) = label_to_index.get(&label) {
1112 return Ok(idx);
1113 }
1114 let idx = node_names.as_ref().map_or(0, Vec::len);
1115 label_to_index.insert(label.clone(), idx);
1116 node_names.as_mut().expect("node names").push(label);
1117 *node_count = (*node_count).max(idx + 1);
1118 Ok(idx)
1119 }
1120 }
1121}
1122
1123fn looks_like_weights(value: &Value, expected_len: usize) -> bool {
1124 match value {
1125 Value::Num(_) | Value::Int(_) => expected_len == 1,
1126 Value::Tensor(tensor) => tensor.data.len() == expected_len,
1127 _ => false,
1128 }
1129}
1130
1131fn weights_from_value(value: &Value, len: usize, name: &'static str) -> BuiltinResult<Vec<f64>> {
1132 let values = numeric_f64_vector(value, name)?;
1133 if values.len() != len {
1134 return Err(graph_error(
1135 name,
1136 format!("{name}: weights must have one value per edge"),
1137 ));
1138 }
1139 for weight in &values {
1140 if !weight.is_finite() || *weight < 0.0 {
1141 return Err(graph_error(
1142 name,
1143 format!("{name}: edge weights must be finite nonnegative values"),
1144 ));
1145 }
1146 }
1147 Ok(values)
1148}
1149
1150fn numeric_vector(value: &Value, name: &'static str) -> BuiltinResult<Vec<usize>> {
1151 let values = numeric_f64_vector(value, name)?;
1152 values
1153 .into_iter()
1154 .map(|value| {
1155 if !value.is_finite() || value < 0.0 || value.fract().abs() > 1e-9 {
1156 Err(graph_error(
1157 name,
1158 format!("{name}: node indices must be nonnegative integers"),
1159 ))
1160 } else {
1161 Ok(value as usize)
1162 }
1163 })
1164 .collect()
1165}
1166
1167fn numeric_f64_vector(value: &Value, name: &'static str) -> BuiltinResult<Vec<f64>> {
1168 match value {
1169 Value::Num(n) => Ok(vec![*n]),
1170 Value::Int(i) => Ok(vec![i.to_f64()]),
1171 Value::Bool(b) => Ok(vec![if *b { 1.0 } else { 0.0 }]),
1172 Value::Tensor(tensor) => Ok(tensor.data.clone()),
1173 Value::LogicalArray(array) => Ok(array
1174 .data
1175 .iter()
1176 .map(|&v| if v != 0 { 1.0 } else { 0.0 })
1177 .collect()),
1178 other => Err(graph_error(
1179 name,
1180 format!("{name}: expected numeric vector, got {other:?}"),
1181 )),
1182 }
1183}
1184
1185fn parse_node_names(
1186 value: &Value,
1187 expected_len: usize,
1188 name: &'static str,
1189) -> BuiltinResult<Vec<String>> {
1190 let names = parse_string_vector(value).map_err(|err| graph_error(name, err))?;
1191 if names.len() != expected_len {
1192 return Err(graph_error(
1193 name,
1194 format!("{name}: node name list length must match node count"),
1195 ));
1196 }
1197 Ok(names)
1198}
1199
1200fn parse_string_vector(value: &Value) -> Result<Vec<String>, String> {
1201 match value {
1202 Value::String(text) => Ok(vec![text.clone()]),
1203 Value::StringArray(array) => Ok(array.data.clone()),
1204 Value::CharArray(chars) if chars.rows == 1 => Ok(vec![chars.data.iter().collect()]),
1205 Value::CharArray(chars) => Ok((0..chars.rows)
1206 .map(|row| {
1207 (0..chars.cols)
1208 .map(|col| chars.data[row + col * chars.rows])
1209 .collect::<String>()
1210 .trim_end()
1211 .to_string()
1212 })
1213 .collect()),
1214 Value::Cell(cell) => cell
1215 .data
1216 .iter()
1217 .map(|entry| {
1218 scalar_text(entry).ok_or_else(|| {
1219 "expected cell array of string or character node names".to_string()
1220 })
1221 })
1222 .collect(),
1223 other => Err(format!(
1224 "expected string, character, or cellstr vector, got {other:?}"
1225 )),
1226 }
1227}
1228
1229fn scalar_text(value: &Value) -> Option<String> {
1230 match value {
1231 Value::String(text) => Some(text.clone()),
1232 Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
1233 Value::CharArray(chars) if chars.rows == 1 => Some(chars.data.iter().collect()),
1234 _ => None,
1235 }
1236}
1237
1238fn canonical(text: &str) -> String {
1239 text.chars()
1240 .filter(|ch| *ch != '_' && *ch != '-' && !ch.is_whitespace())
1241 .flat_map(char::to_lowercase)
1242 .collect()
1243}
1244
1245fn positive_integer_scalar(value: &Value, name: &'static str) -> BuiltinResult<usize> {
1246 let values = numeric_vector(value, name)?;
1247 if values.len() != 1 {
1248 return Err(graph_error(
1249 name,
1250 format!("{name}: expected scalar node count"),
1251 ));
1252 }
1253 Ok(values[0])
1254}
1255
1256fn node_index_from_value(
1257 graph: &GraphData,
1258 value: &Value,
1259 name: &'static str,
1260) -> BuiltinResult<usize> {
1261 match value {
1262 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) | Value::Cell(_) => {
1263 let labels = parse_string_vector(value).map_err(|err| graph_error(name, err))?;
1264 if labels.len() != 1 {
1265 return Err(graph_error(name, format!("{name}: expected one node")));
1266 }
1267 let Some(names) = &graph.node_names else {
1268 return Err(graph_error(
1269 name,
1270 format!("{name}: string node references require named graph nodes"),
1271 ));
1272 };
1273 names
1274 .iter()
1275 .position(|candidate| candidate == &labels[0])
1276 .ok_or_else(|| graph_error(name, format!("{name}: unknown node '{}'", labels[0])))
1277 }
1278 _ => {
1279 let idx = positive_integer_scalar(value, name)?;
1280 node_index_from_one_based(idx, graph.node_count, name)
1281 }
1282 }
1283}
1284
1285fn node_indices_from_value(
1286 graph: &GraphData,
1287 value: &Value,
1288 name: &'static str,
1289) -> BuiltinResult<Vec<usize>> {
1290 match value {
1291 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) | Value::Cell(_) => {
1292 let labels = parse_string_vector(value).map_err(|err| graph_error(name, err))?;
1293 let Some(names) = &graph.node_names else {
1294 return Err(graph_error(
1295 name,
1296 format!("{name}: string node references require named graph nodes"),
1297 ));
1298 };
1299 labels
1300 .into_iter()
1301 .map(|label| {
1302 names
1303 .iter()
1304 .position(|candidate| candidate == &label)
1305 .ok_or_else(|| graph_error(name, format!("{name}: unknown node '{label}'")))
1306 })
1307 .collect()
1308 }
1309 _ => numeric_vector(value, name)?
1310 .into_iter()
1311 .map(|idx| node_index_from_one_based(idx, graph.node_count, name))
1312 .collect(),
1313 }
1314}
1315
1316fn node_index_from_f64(value: f64, node_count: usize, name: &'static str) -> BuiltinResult<usize> {
1317 if !value.is_finite() || value < 1.0 || value.fract().abs() > 1e-9 {
1318 return Err(graph_error(
1319 name,
1320 format!("{name}: edge endpoints must be positive integer node ids"),
1321 ));
1322 }
1323 node_index_from_one_based(value as usize, node_count, name)
1324}
1325
1326fn node_index_from_one_based(
1327 value: usize,
1328 node_count: usize,
1329 name: &'static str,
1330) -> BuiltinResult<usize> {
1331 if value == 0 || value > node_count {
1332 return Err(graph_error(
1333 name,
1334 format!("{name}: node index is outside the graph"),
1335 ));
1336 }
1337 Ok(value - 1)
1338}
1339
1340fn edge_matches(edge: &Edge, source: usize, target: usize, directed: bool) -> bool {
1341 (edge.source == source && edge.target == target)
1342 || (!directed && edge.source == target && edge.target == source)
1343}
1344
1345fn adjacency_tensor(graph: &GraphData, name: &'static str) -> BuiltinResult<Tensor> {
1346 let n = graph.node_count;
1347 let mut data = vec![0.0; n * n];
1348 for edge in &graph.edges {
1349 data[edge.source + edge.target * n] += edge.weight;
1350 if !graph.directed && edge.source != edge.target {
1351 data[edge.target + edge.source * n] += edge.weight;
1352 }
1353 }
1354 Tensor::new(data, vec![n, n]).map_err(|err| internal_error(name, err))
1355}
1356
1357fn degree_counts(graph: &GraphData) -> Vec<f64> {
1358 if graph.directed {
1359 outdegree_counts(graph)
1360 .into_iter()
1361 .zip(indegree_counts(graph))
1362 .map(|(out, incoming)| out + incoming)
1363 .collect()
1364 } else {
1365 let mut counts = vec![0.0; graph.node_count];
1366 for edge in &graph.edges {
1367 if edge.source == edge.target {
1368 counts[edge.source] += 2.0;
1369 } else {
1370 counts[edge.source] += 1.0;
1371 counts[edge.target] += 1.0;
1372 }
1373 }
1374 counts
1375 }
1376}
1377
1378fn indegree_counts(graph: &GraphData) -> Vec<f64> {
1379 if !graph.directed {
1380 return degree_counts_undirected(graph);
1381 }
1382 let mut counts = vec![0.0; graph.node_count];
1383 for edge in &graph.edges {
1384 counts[edge.target] += 1.0;
1385 }
1386 counts
1387}
1388
1389fn outdegree_counts(graph: &GraphData) -> Vec<f64> {
1390 if !graph.directed {
1391 return degree_counts_undirected(graph);
1392 }
1393 let mut counts = vec![0.0; graph.node_count];
1394 for edge in &graph.edges {
1395 counts[edge.source] += 1.0;
1396 }
1397 counts
1398}
1399
1400fn degree_counts_undirected(graph: &GraphData) -> Vec<f64> {
1401 let mut counts = vec![0.0; graph.node_count];
1402 for edge in &graph.edges {
1403 counts[edge.source] += 1.0;
1404 if edge.source != edge.target {
1405 counts[edge.target] += 1.0;
1406 }
1407 }
1408 counts
1409}
1410
1411fn node_metric_value(values: Vec<f64>, name: &'static str) -> BuiltinResult<Value> {
1412 let len = values.len();
1413 tensor_value(values, vec![len, 1], name)
1414}
1415
1416#[derive(Clone, Copy)]
1417enum NeighborMode {
1418 Undirected,
1419 Successors,
1420 Predecessors,
1421}
1422
1423async fn neighbor_query(
1424 graph: Value,
1425 node: Value,
1426 mode: NeighborMode,
1427 name: &'static str,
1428) -> BuiltinResult<Value> {
1429 let graph = graph_data(&graph, name)?;
1430 let node = gather_if_needed_async(&node).await?;
1431 let node = node_index_from_value(&graph, &node, name)?;
1432 let mut neighbors = neighbors_for(&graph, node, mode);
1433 neighbors.sort_unstable();
1434 neighbors.dedup();
1435 node_list_value(&graph, &neighbors, name)
1436}
1437
1438fn neighbors_for(graph: &GraphData, node: usize, mode: NeighborMode) -> Vec<usize> {
1439 let mut out = Vec::new();
1440 for edge in &graph.edges {
1441 match mode {
1442 NeighborMode::Undirected => {
1443 if edge.source == node {
1444 out.push(edge.target);
1445 }
1446 if edge.target == node && edge.source != node {
1447 out.push(edge.source);
1448 }
1449 }
1450 NeighborMode::Successors => {
1451 if edge.source == node {
1452 out.push(edge.target);
1453 }
1454 if !graph.directed && edge.target == node && edge.source != node {
1455 out.push(edge.source);
1456 }
1457 }
1458 NeighborMode::Predecessors => {
1459 if edge.target == node {
1460 out.push(edge.source);
1461 }
1462 if !graph.directed && edge.source == node && edge.target != node {
1463 out.push(edge.target);
1464 }
1465 }
1466 }
1467 }
1468 out
1469}
1470
1471fn adjacency_lists(graph: &GraphData, mode: NeighborMode) -> Vec<Vec<usize>> {
1472 let mut lists = vec![Vec::new(); graph.node_count];
1473 for node in 0..graph.node_count {
1474 let mut neighbors = neighbors_for(graph, node, mode);
1475 neighbors.sort_unstable();
1476 neighbors.dedup();
1477 lists[node] = neighbors;
1478 }
1479 lists
1480}
1481
1482fn weighted_adjacency(graph: &GraphData) -> Vec<Vec<(usize, f64)>> {
1483 let mut lists = vec![Vec::new(); graph.node_count];
1484 for edge in &graph.edges {
1485 lists[edge.source].push((edge.target, edge.weight));
1486 if !graph.directed && edge.source != edge.target {
1487 lists[edge.target].push((edge.source, edge.weight));
1488 }
1489 }
1490 lists
1491}
1492
1493fn dijkstra(graph: &GraphData, source: usize) -> (Vec<f64>, Vec<Option<usize>>) {
1494 let adjacency = weighted_adjacency(graph);
1495 let mut distances = vec![f64::INFINITY; graph.node_count];
1496 let mut previous = vec![None; graph.node_count];
1497 let mut heap = BinaryHeap::new();
1498 distances[source] = 0.0;
1499 heap.push(QueueItem {
1500 distance: 0.0,
1501 node: source,
1502 });
1503 while let Some(QueueItem { distance, node }) = heap.pop() {
1504 if distance > distances[node] {
1505 continue;
1506 }
1507 for &(next, weight) in &adjacency[node] {
1508 let candidate = distance + weight;
1509 if candidate < distances[next] {
1510 distances[next] = candidate;
1511 previous[next] = Some(node);
1512 heap.push(QueueItem {
1513 distance: candidate,
1514 node: next,
1515 });
1516 }
1517 }
1518 }
1519 (distances, previous)
1520}
1521
1522fn reconstruct_path(source: usize, target: usize, previous: &[Option<usize>]) -> Vec<usize> {
1523 if source == target {
1524 return vec![source];
1525 }
1526 let mut path = Vec::new();
1527 let mut current = target;
1528 path.push(current);
1529 while let Some(parent) = previous[current] {
1530 current = parent;
1531 path.push(current);
1532 if current == source {
1533 path.reverse();
1534 return path;
1535 }
1536 }
1537 Vec::new()
1538}
1539
1540fn node_list_value(graph: &GraphData, nodes: &[usize], name: &'static str) -> BuiltinResult<Value> {
1541 if let Some(names) = &graph.node_names {
1542 Ok(Value::StringArray(
1543 StringArray::new(
1544 nodes.iter().map(|&idx| names[idx].clone()).collect(),
1545 vec![nodes.len(), 1],
1546 )
1547 .map_err(|err| internal_error(name, err))?,
1548 ))
1549 } else {
1550 tensor_value(
1551 nodes.iter().map(|&idx| (idx + 1) as f64).collect(),
1552 vec![nodes.len(), 1],
1553 name,
1554 )
1555 }
1556}
1557
1558fn assign_tree_positions(
1559 node: usize,
1560 depth: f64,
1561 children: &[Vec<usize>],
1562 leaf_cursor: &mut f64,
1563 x: &mut [f64],
1564 y: &mut [f64],
1565) {
1566 y[node] = -depth;
1567 if children[node].is_empty() {
1568 x[node] = *leaf_cursor;
1569 *leaf_cursor += 1.0;
1570 return;
1571 }
1572 for &child in &children[node] {
1573 assign_tree_positions(child, depth + 1.0, children, leaf_cursor, x, y);
1574 }
1575 let first = children[node].first().copied().unwrap();
1576 let last = children[node].last().copied().unwrap();
1577 x[node] = (x[first] + x[last]) / 2.0;
1578}
1579
1580fn tensor_value(data: Vec<f64>, shape: Vec<usize>, name: &'static str) -> BuiltinResult<Value> {
1581 Ok(tensor::tensor_into_value(
1582 Tensor::new(data, shape).map_err(|err| internal_error(name, err))?,
1583 ))
1584}
1585
1586fn tensor_value_raw(data: Vec<f64>, shape: Vec<usize>, name: &'static str) -> BuiltinResult<Value> {
1587 Ok(Value::Tensor(
1588 Tensor::new(data, shape).map_err(|err| internal_error(name, err))?,
1589 ))
1590}
1591
1592#[derive(Clone, Copy)]
1593enum TriangleMode {
1594 Full,
1595 Upper,
1596 Lower,
1597}
1598
1599fn ensure_graph_classes_registered() {
1600 static REGISTER: OnceLock<()> = OnceLock::new();
1601 REGISTER.get_or_init(|| {
1602 register_graph_class(GRAPH_CLASS);
1603 register_graph_class(DIGRAPH_CLASS);
1604 });
1605}
1606
1607fn register_graph_class(name: &str) {
1608 let mut properties = HashMap::new();
1609 for property_name in ["Edges", "Nodes", NUM_NODES_PROPERTY] {
1610 properties.insert(
1611 property_name.to_string(),
1612 PropertyDef {
1613 name: property_name.to_string(),
1614 is_static: false,
1615 is_constant: false,
1616 is_dependent: false,
1617 get_access: Access::Public,
1618 set_access: Access::Public,
1619 default_value: None,
1620 },
1621 );
1622 }
1623 runmat_builtins::register_class(ClassDef {
1624 name: name.to_string(),
1625 parent: None,
1626 properties,
1627 methods: HashMap::<String, MethodDef>::new(),
1628 });
1629}
1630
1631#[cfg(test)]
1632mod tests {
1633 use super::*;
1634 use futures::executor::block_on;
1635
1636 fn numeric(data: Vec<f64>, rows: usize, cols: usize) -> Value {
1637 Value::Tensor(Tensor::new(data, vec![rows, cols]).expect("tensor"))
1638 }
1639
1640 fn tensor_data(value: Value) -> Vec<f64> {
1641 match value {
1642 Value::Tensor(t) => t.data,
1643 Value::Num(n) => vec![n],
1644 other => panic!("expected numeric value, got {other:?}"),
1645 }
1646 }
1647
1648 #[test]
1649 fn graph_constructor_builds_edges_nodes_and_adjacency() {
1650 let graph = block_on(graph_builtin(vec![
1651 numeric(vec![1.0, 2.0, 3.0], 3, 1),
1652 numeric(vec![2.0, 3.0, 1.0], 3, 1),
1653 ]))
1654 .expect("graph");
1655 assert_eq!(
1656 block_on(numnodes_builtin(graph.clone())).unwrap(),
1657 Value::Num(3.0)
1658 );
1659 assert_eq!(
1660 block_on(numedges_builtin(graph.clone())).unwrap(),
1661 Value::Num(3.0)
1662 );
1663
1664 let adjacency = block_on(adjacency_builtin(graph)).expect("adjacency");
1665 assert_eq!(
1666 tensor_data(adjacency),
1667 vec![0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0]
1668 );
1669 }
1670
1671 #[test]
1672 fn digraph_preserves_direction_for_neighbors_and_degree() {
1673 let graph = block_on(digraph_builtin(vec![
1674 numeric(vec![1.0, 1.0, 2.0], 3, 1),
1675 numeric(vec![2.0, 3.0, 3.0], 3, 1),
1676 ]))
1677 .expect("digraph");
1678 assert_eq!(
1679 tensor_data(block_on(outdegree_builtin(graph.clone())).unwrap()),
1680 vec![2.0, 1.0, 0.0]
1681 );
1682 assert_eq!(
1683 tensor_data(block_on(indegree_builtin(graph.clone())).unwrap()),
1684 vec![0.0, 1.0, 2.0]
1685 );
1686 assert_eq!(
1687 tensor_data(block_on(successors_builtin(graph.clone(), Value::Num(1.0))).unwrap()),
1688 vec![2.0, 3.0]
1689 );
1690 assert_eq!(
1691 tensor_data(block_on(predecessors_builtin(graph, Value::Num(3.0))).unwrap()),
1692 vec![1.0, 2.0]
1693 );
1694 }
1695
1696 #[test]
1697 fn named_graph_returns_named_paths() {
1698 let names = Value::StringArray(
1699 StringArray::new(vec!["a".into(), "b".into(), "c".into()], vec![3, 1]).expect("names"),
1700 );
1701 let graph = block_on(graph_builtin(vec![
1702 numeric(vec![1.0, 2.0], 2, 1),
1703 numeric(vec![2.0, 3.0], 2, 1),
1704 numeric(vec![2.0, 1.0], 2, 1),
1705 names,
1706 ]))
1707 .expect("graph");
1708 let path = block_on(shortestpath_builtin(
1709 graph,
1710 Value::CharArray(runmat_builtins::CharArray::new_row("a")),
1711 Value::CharArray(runmat_builtins::CharArray::new_row("c")),
1712 ))
1713 .expect("path");
1714 let Value::StringArray(path) = path else {
1715 panic!("expected named path");
1716 };
1717 assert_eq!(path.data, vec!["a", "b", "c"]);
1718 }
1719
1720 #[test]
1721 fn adjacency_constructor_and_distances_use_weights() {
1722 let graph = block_on(digraph_builtin(vec![numeric(
1723 vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 10.0, 1.0, 0.0],
1724 3,
1725 3,
1726 )]))
1727 .expect("digraph");
1728 let distances = block_on(distances_builtin(
1729 graph,
1730 vec![Value::Num(1.0), Value::Num(3.0)],
1731 ))
1732 .expect("distances");
1733 assert_eq!(distances, Value::Num(3.0));
1734 }
1735
1736 #[test]
1737 fn undirected_adjacency_constructor_accepts_lower_triangle() {
1738 let graph =
1739 block_on(graph_builtin(vec![numeric(vec![0.0, 5.0, 0.0, 0.0], 2, 2)])).expect("graph");
1740 assert_eq!(
1741 block_on(numedges_builtin(graph.clone())).unwrap(),
1742 Value::Num(1.0)
1743 );
1744 assert_eq!(
1745 tensor_data(block_on(adjacency_builtin(graph)).unwrap()),
1746 vec![0.0, 5.0, 5.0, 0.0]
1747 );
1748 }
1749
1750 #[test]
1751 fn treelayout_returns_single_or_pair_by_output_count() {
1752 let result = block_on(treelayout_builtin(numeric(vec![0.0, 1.0, 1.0, 2.0], 1, 4)))
1753 .expect("treelayout");
1754 assert_eq!(tensor_data(result).len(), 4);
1755
1756 let _guard = crate::output_count::push_output_count(Some(2));
1757 let result = block_on(treelayout_builtin(numeric(vec![0.0, 1.0, 1.0, 2.0], 1, 4)))
1758 .expect("treelayout");
1759 let Value::OutputList(values) = result else {
1760 panic!("expected output list");
1761 };
1762 assert_eq!(values.len(), 2);
1763 assert_eq!(tensor_data(values[0].clone()).len(), 4);
1764 assert_eq!(tensor_data(values[1].clone()), vec![-0.0, -1.0, -1.0, -2.0]);
1765 }
1766}