Skip to main content

runmat_runtime/builtins/graph/
mod.rs

1//! MATLAB-compatible graph and digraph data-type helpers.
2
3use 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}