graph, digraph, GraphPlot classes (MATLAB parity)#57
Conversation
Empty category directory. Populated by subsequent stories implementing the graph, digraph, and GraphPlot classdef files. Follows the same module.mk pattern as scripts/audio/ with an initial .oct-config only.
…-I03) Private helper that loads a MATLAB reference-output JSON fixture from the sibling MatSlop tree (default) or a caller-supplied directory, so %!test blocks in digraph/graph methods can assert byte-for-byte parity with MATLAB's documented output. Returns an empty struct + warning when the fixture is absent so tests can skip gracefully when run outside the MatSlop development layout (e.g. an upstream CI builder with no MatSlop repo). * scripts/graph/private/__matlab_ref__.m: new helper, GPLv3 header, texinfo docstring, 5 %!test / %!error BIST assertions. * scripts/graph/module.mk: wire private/ into FCN_FILE_DIRS and FCN_FILES. All BIST pass (5/5).
* scripts/graph/digraph.m: New classdef. digraph() returns an empty directed graph; digraph(N) with a non-negative integer scalar N returns an N-node edgeless graph. Storage is a private sparse adjacency matrix plus a cellstr of node names. numnodes and numedges implemented as classdef methods. Full GPLv3 header and texinfo docstring with @deftypefn/@deftypefnx, @example/@group, @Seealso. 13 %!test blocks cover default, N-node, 0, 1, and 1000 node cases plus input-validation %!error blocks for negative, non-integer, non-scalar, Inf, NaN, and too-many-argument calls. * scripts/graph/module.mk: Add digraph.m to scripts_graph_FCN_FILES.
Extend digraph constructor to accept two numeric vectors S and T of equal length, building one directed edge S(i) -> T(i) per index with node count auto-set to max(max(S), max(T)). S and T must be positive integer vectors (MATLAB is 1-based); empty S and T yield an empty digraph. Adds 23 BIST blocks covering row/column orientation, empty input, self-loop, siever-style 9-node/12-edge fixture, and all validation errors. 36/36 BIST pass against Octave 9.4.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend digraph(s, t) to accept an optional weight vector or scalar w: digraph(s, t, w) w may be a scalar (broadcast to every edge) or a numeric real vector of length numel(s). Weights are rejected if they contain NaN, are complex, are non-numeric, or are a non-vector non-scalar. Introduce has_weights_ private property plus a Dependent, SetAccess = private Edges property that returns a struct with EndNodes (m-by-2) and, when the graph was constructed with weights, Weight (m-by-1). Unweighted graphs have no Weight field — matches MATLAB parity until a real table class is available. Edges are reported in lexicographic (source, destination) order via find(adj_.') regardless of construction order. 21 new %!test/%!error blocks; 57/57 BIST green.
Accept a fourth cellstr argument naming every node: G = digraph (s, t, w, nodenames) Entries of s and t may be numeric (treated as 1-based indices into nodenames) or strings / cellstr (looked up in nodenames). Node count is numel (nodenames) regardless of the maximum endpoint index, so isolated named nodes are preserved. Pass [] for w to create an unweighted named digraph. G.Nodes returns a struct with a Name column (cellstr). Adds a new private helper scripts/graph/private/__resolve_endpoint__.m that converts the user-supplied endpoint vector into a column of node indices. The helper will also be reused by graph.m. 76/76 BIST green on digraph.m (19 new US-C04 tests), 9/9 on the new helper, __matlab_ref__ still 5/5.
Extend the four-argument digraph constructor to accept a non-negative integer scalar N as its fourth argument, in addition to the cellstr nodenames form added in US-C04. digraph(s, t, w, N) creates a digraph with exactly N nodes; indices in s and t greater than max endpoint but not exceeding N correspond to isolated trailing nodes. Pass [] for W to construct an unweighted digraph with N nodes. The nargin==4 branch is now a type dispatcher: iscellstr(arg4) routes to the existing nodenames path, isnumeric+isscalar(arg4) routes to the new N path, and anything else errors out with a MATLAB-compatible message describing both accepted forms. The N path reuses the full 2/3-argument s/t/w validation (numeric-real vectors, matching length, positive-integer entries, W numeric-real vector-or-scalar, NaN rejected) and adds a range check so endpoints exceeding N are rejected. 31 new %!test / %!error BIST blocks cover the happy path, scalar weight broadcast, empty-endpoint and N=0 cases, edge lex order with trailing isolates, a Siever-style 12-edge fixture padded to 20 nodes, every input-validation error branch, and the fourth-argument type check. test digraph: 107/107 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend the single-argument constructor to accept a square numeric or logical matrix A and build the digraph from its adjacency pattern: every nonzero A(i,j) becomes an edge i->j with weight A(i,j). The nargin==1 branch is now a dispatcher -- scalar numeric stays the node count form (validation unchanged), non-scalar 2-D numeric/logical routes to the new adjacency path, and anything else (3-D, cell, ...) errors with a MATLAB-style message. Adjacency validation rejects complex (<real>), non-square (<square>), and NaN (<NaN>) inputs; sparse input is preserved without densifying so large sparse adjacency matrices work. Matrix form always carries a Weight column (MATLAB parity), while the 0x0 case stays unweighted. int8/logical inputs are coerced to double in G.Edges.Weight. 131/131 BIST green (24 new tests + 1 updated: the old '<non-negative integer> digraph ([1 2 3])' now expects '<square>' because a 1x3 row vector is a non-square adjacency matrix). __resolve_endpoint__ 9/9, __matlab_ref__ 5/5. No regressions.
Add digraph (A, NODENAMES) as an eighth overload of the digraph constructor. A is a real square numeric or logical matrix (dense or sparse); NODENAMES is a cellstr of unique strings with numel (NODENAMES) == size (A, 1). Semantics otherwise match the adjacency-matrix form introduced in US-C06: each nonzero A(i,j) becomes an edge i->j with weight A(i,j), sparse input is preserved, self-loops are permitted, and size(A,1)>0 implies the Edges struct carries a Weight column (MATLAB parity). Dispatch works at the nargin == 2 level: if the second argument is a cellstr, route to the new branch; otherwise fall through to the existing digraph (s, t) edge-list path. That is unambiguous because edge-list requires a numeric destination vector. Validation error regexes: <real>, <square>, <NaN>, <unique>, <numel>. 23 new %!test / %!error blocks cover dense/sparse/logical/int8 adjacency, isolated-node preservation, self-loops, 0x0 edge case, negative weights, a Siever-style 9-node fixture, duplicate-name rejection, size mismatch, non-square, complex, and NaN-in-A errors. 154/154 BIST green; __resolve_endpoint__ 9/9, __matlab_ref__ 5/5, no regression. Commit was driven by the Ralph TDD loop against the installed Octave 9.4.0 on Windows; make check skipped as in prior stories for lack of a configure/make toolchain on this host.
Accept scalar structs standing in for MATLAB tables (Octave has no
table class yet):
* digraph (ET) -- ET has EndNodes (m-by-2 numeric or cellstr) and
optional Weight; any other fields are preserved as extra edge
columns on G.Edges.
* digraph (ET, NT) -- NT has an optional Name cellstr plus any
extra columns preserved on G.Nodes.
Edges are re-sorted into lexicographic (source, destination) order
and every extra edge column is reordered to match via a permutation
recovered from a sparse(s, t, 1:m, N, N) build. Duplicate (s, t)
pairs are detected via nnz (p) != m and rejected (a future
'multigraph' flag will permit parallel edges). Cellstr EndNodes
resolve via __resolve_endpoint__ using NT.Name when supplied, or via
first-appearance inference otherwise.
Two new private properties carry the user's extra columns:
* edge_attrs_ (lex-ordered)
* node_attrs_ (node-index-ordered)
get.Edges and get.Nodes merge them into the returned struct.
37 new %!test / %!error blocks; digraph 191/191 green,
__resolve_endpoint__ 9/9, __matlab_ref__ 5/5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every digraph(...) constructor form now accepts a trailing "omitselfloops" string (case-insensitive). When present, every self-loop edge (i, i) is dropped after the rest of the graph is built. Extra edge-attribute columns supplied via the EdgeTable form are filtered by the same mask so row counts stay in sync. Implementation: constructor pre-processes varargin to strip the flag into a local omit_loops boolean; local args/nargs shadow the built-ins so the eight dispatch branches keep their shape. Tail post-processing rebuilds adj_ via sparse(r(keep), c(keep), ...) and uses sortrows([r, c]) on the existing find() output to derive the lex-order mask for edge_attrs_ (cheaper than a second find on the transpose). 22 new BIST blocks exercise every constructor form, case variants, edge cases (all self-loops, no self-loops, empty edges, isolated N), and error paths; digraph 213/213 green, __resolve_endpoint__ 9/9, __matlab_ref__ 5/5.
Introduce the undirected graph class mirroring US-C01..US-C05 of the
digraph work. Supported signatures:
graph () empty undirected graph
graph (N) N isolated nodes
graph (S, T) edge-list, unweighted
graph (S, T, W) edge-list with scalar or
vector weights
graph (S, T, W, NODENAMES) named nodes; string endpoints
resolved via NODENAMES
graph (S, T, W, N) explicit node count; isolated
trailing nodes preserved
Edges are stored in a symmetric sparse adjacency matrix: each
off-diagonal undirected edge contributes a pair of symmetric entries,
and self-loops contribute a single diagonal entry. Duplicate
unordered endpoint pairs are rejected by normalising to (min, max)
and sparse-accumulating the 1..m index sequence. G.Edges.EndNodes
returns rows in lexicographic (s, t) order with s <= t via
find (tril (adj_)). numedges = nnz (tril (adj_)).
Test coverage: 78 new %!test / %!error BIST blocks covering all six
call forms, weight validation, node-name resolution, duplicate-edge
detection, isolated trailing nodes, and value-class copy semantics.
digraph 243/243 and __resolve_endpoint__ 9/9 remain green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…S-C12)
Extend the scripts/graph/graph.m constructor to accept an adjacency
matrix as the sole argument, plus an optional 'upper' or 'lower'
triangle selector:
- graph (A): A must be a real square numeric or logical matrix and
must be symmetric. A non-symmetric A errors with a MATLAB-style
message that hints at 'upper'/'lower'. Sparse A stays sparse;
logical and integer A are coerced to double. NaN rejected.
- graph (A, 'upper') uses only triu (A); the strictly-upper half is
mirrored across the diagonal into the strictly-lower half so the
internal symmetric storage is complete. Self-loops (diagonal)
are preserved.
- graph (A, 'lower') analogous for tril (A).
Dispatch changes:
* nargs == 1 now routes scalars to the existing N-node path and
non-scalar 2-D numeric/logical matrices to the new adjacency
path.
* nargs == 2 with a char-row second argument AND a non-vector
matrix first argument routes to the new triangle-flag path.
Vector first argument falls through to the existing edge-list
branch, keeping graph ([1 2], "ab") -> 'S and T must be
numeric vectors'.
A new local helper build_adj_from_matrix (A, mode) centralises the
real/square/NaN validation and the type coercion. Matrix form
always carries a Weight column (MATLAB parity); 0x0 adjacency
stays unweighted.
Updated one legacy BIST: `%!error <non-negative integer> graph
([1 2 3])` is now `%!error <square> graph ([1 2 3])` because
[1 2 3] is a 1x3 matrix that now dispatches to the adjacency
path -- MATLAB behaves the same way ("Adjacency matrix must be
square").
Added 49 new %!test / %!error BIST blocks. Docstring gained three
new @deftypefnx synopses and two worked examples.
`test graph` -> 127/127; no regression in digraph (243/243),
__resolve_endpoint__ (9/9), or __matlab_ref__ (5/5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the digraph US-C08 EdgeTable/NodeTable constructor for the undirected graph class. A new dispatch branch above the existing nargs==1 scalar/matrix dispatcher routes `(nargs == 1 && isstruct(arg1))` or `(nargs == 2 && isstruct(arg1) && isstruct(arg2))` to the new form, disjoint from the existing adjacency+triangle-flag branch. Validation mirrors digraph US-C08 with the `graph:` prefix: scalar struct ET with required EndNodes (m-by-2 numeric or cellstr) and optional Weight; any other fields become extra edge-attribute columns. Optional scalar struct NT with optional cellstr Name and any other fields as extra node-attribute columns. Two new private properties `edge_attrs_` and `node_attrs_` carry the extra columns; get.Edges and get.Nodes merge them into the returned struct. Graph-specific undirected-pair normalization: `s_n = min(s, t); t_n = max(s, t); p = sparse(t_n, s_n, 1:m, N, N)` builds a lower-triangular sparse whose `nnz(p) != m` check detects duplicates regardless of input orientation -- including undirected duplicates like (1,2) and (2,1) which normalize to the same (min, max) cell. find(p) walks column-major through the lower triangle, yielding lex (s_n, t_n) order -- the same ordering contract get.Edges uses via find(tril(adj_)) -- so the permutation extracted from find(p)'s third return reorders every extra edge column into the same lex order that Weight lands in naturally via adj_ storage. adj_ is built via the existing build_adj local helper. To avoid the constructor running a second redundant O(m log m) sparse build inside build_adj, build_adj gained an optional sixth `skip_dup_check` parameter; nargin<6 defaults to false so the five existing call sites are unchanged. Docstring updated with @deftypefnx lines for `graph (EDGETABLE)` and `graph (EDGETABLE, NODETABLE)`, an explanatory section covering unordered-pair normalization, Weight-follows-edge semantics, and first-appearance name inference, plus an example in the examples block. 39 new %!test/%!error blocks (37 mirroring the digraph US-C08 suite plus 2 graph-specific for unordered-pair normalization and undirected-duplicate detection). graph 166/166 BIST green; digraph 243/243, __resolve_endpoint__ 9/9, __matlab_ref__ 5/5 -- no regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…C14) The Nodes and Edges getters have existed since US-C03/C04 (digraph) and US-C11 (graph); US-C14 pulls the coverage into a dedicated BIST section and closes the last read-access gaps. get.Edges now forces an m-by-2 EndNodes and m-by-1 Weight column for every adjacency size via a (:) coercion. Previously find() on a 0-by-0 sparse matrix returned 0-by-0 arrays, so the truly empty graph (digraph(), graph(), graph([],[]), etc.) leaked a 0-by-0 EndNodes -- inconsistent with every non-empty case and with MATLAB. The coercion is a no-op on non-empty graphs; multigraph storage was already correct via zeros(0,2) defaults. Added a Properties section to both texinfo docstrings describing the two read-only struct views (always-present fields, field order, read-only access, user-column preservation). 43 new BIST blocks (22 on digraph.m, 21 on graph.m) cover: struct return type on every constructor form; Name always-present as column cellstr; EndNodes always m-by-2 numeric; Weight presence gated on construction; fieldnames order; SetAccess=private; idempotent getter; dynamic field access; fully-featured round-trip. digraph 265/265, graph 186/186, __resolve_endpoint__ 9/9, __matlab_ref__ 5/5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a `disp (G)` method to both scripts/graph/digraph.m and scripts/graph/graph.m that prints a concise, human-readable summary instead of Octave's default classdef "object with properties:" output. The header follows the PRD-specified format "digraph with N nodes and M edges" / "graph with N nodes and M edges" with singular/plural word handling for 1 node / 1 edge. An empty graph terminates the header with a period; a non-empty graph follows the header with a preview of up to the first 10 edges in a simple columnar format (left-aligned EndNode1/EndNode2 auto-widened to the widest label, optional right-aligned Weight column when the graph is weighted). A continuation line "... (N more edges)" reports any truncation. Node names are printed in place of numeric indices whenever the graph was constructed with names. Octave's default classdef display delegates to disp after printing the "G =\n\n" assignment prefix, so no separate display method is needed. 26 new %!test blocks (13 per class) cover: empty graph header, N-node edgeless graph, weighted 3-edge graph, singular plural forms, many-edge truncation with continuation line, named-graph labels, multigraph parallel-edge counting (digraph only), Weight column presence/absence, display-prefix, and graph-vs-digraph header prefix specificity. Result: digraph 278/278 green (+13), graph 199/199 green (+13); __resolve_endpoint__ 9/9, __matlab_ref__ 5/5; no regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New scripts/graph/numnodes.m and scripts/graph/numedges.m provide top-level documentation targets shared by graph and digraph: each has a full texinfo docstring with example and @Seealso, plus 23 BIST blocks covering empty, N-node, edge-list, weighted, named-node, self-loop, isolated-node, siever, multigraph, and adjacency-matrix cases across both classes, with six %!error blocks for non-graph inputs. The function body runs only for non-graph inputs (classdef method dispatch intercepts graph/digraph calls): nargin check, `isa(G,'graph') || isa(G,'digraph')` guard, and a defensive dot-notation delegation fall-through. The existing classdef methods remain unchanged. Regression clean: numnodes 23/23, numedges 23/23, digraph 278/278, graph 199/199.
Add scripts/graph/successors.m and scripts/graph/predecessors.m as standalone files (free-function delegates that route to classdef methods on digraph objects, and error with a helpful message on non-digraph inputs), matching the two-surface pattern established by numnodes/numedges in US-Q01. Both queries accept a scalar NODEID that is either a numeric 1-based node index or a node name (char row vector or 1-element cellstr), and return a column vector whose type matches the input: numeric indices in / numeric indices out, string in / cellstr of names out (MATLAB parity). New classdef methods digraph.successors and digraph.predecessors dispatch on is_multigraph_: multigraph storage (lex-sorted mg_endnodes_) returns filtered column 2 (successors) or column 1 (predecessors) directly, already in sorted order; simple-graph storage uses find (adj_(n, :)) or find (adj_(:, n)) respectively. Parallel edges in a multigraph contribute duplicate entries, matching MATLAB. Return-type matching lives in a new private helper scripts/graph/private/__resolve_single_node__.m that takes (G, nodeID, method) and returns [idx, by_name] where by_name signals whether the caller passed a string identifier. The helper reads through the public G.Nodes.Name surface so later stories can also call it from graph.m methods (e.g. neighbors in US-Q03) without duplicating the validation logic. Full input validation with %!error coverage: out-of-range, non-integer, non-positive, non-existent name, name on unnamed digraph, non-scalar numeric, multi-element cellstr, non-digraph first argument (including graph objects), and nargin mismatch. Coverage: successors 28/28, predecessors 28/28, __resolve_single_node__ 12/12. No regression: digraph 278/278, graph 199/199, numnodes 23/23, numedges 23/23, __resolve_endpoint__ 9/9, __matlab_ref__ 5/5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a new neighbors(G, nodeID) query supported on both the graph and digraph classes. For undirected graph, neighbors returns the incident nodes (find(adj_(n,:)), already sorted). For digraph, neighbors returns the union of successors and predecessors, sorted ascending; a self-loop contributes the node once. For a digraph multigraph, each parallel edge contributes one entry so duplicate neighbours are possible; a self-loop stored as a single (n,n) edge contributes n once. NodeID resolution reuses the existing __resolve_single_node__ helper, so numeric indices return doubles and char / 1-element cellstr return a column cellstr of node names. New file scripts/graph/neighbors.m holds the canonical help/docstring target, a non-graph input guard, and 56 %!test/%!error BIST blocks spanning both classes and all input variants. Top-level graph and digraph docstring @Seealso lists extended to mention neighbors.
New scripts/graph/indegree.m and scripts/graph/outdegree.m each carry the 24-line GPLv3 header, a full texinfo docstring with the two synopses (no-arg full-vector form and NODEIDS subset form), an example, and @Seealso cross-references. Free-function bodies guard nargin and the G-is-digraph isa check, then delegate to the classdef method via dot-notation. Two new classdef methods digraph.indegree and digraph.outdegree: simple-graph path uses full(sum(spones(adj_), 1 or 2))(:) so weighted digraphs still report edge counts not weight sums; multigraph path uses accumarray on mg_endnodes_ column 2 (in) or column 1 (out). The nargin==1 form returns a numnodes-by-1 column vector; the nargin==2 form resolves NODEIDS and returns a result reshaped to the input's shape (scalar stays scalar, row stays row, column stays column, any 2-D array preserved). New private helper scripts/graph/private/__resolve_node_list__.m handles the vector-capable resolution: numeric array of positive integer indices (any shape), char row vector (single name -> [1 1] shape), cellstr of any shape, or empty [] / {}. Returns idx as a column vector plus out_shape so callers can reshape the result. Error messages prefixed with the caller's method name for clearer diagnostics. 70 new BIST blocks (35 per function) covering: empty digraph, edgeless N-node, simple digraph all-nodes and subset forms, scalar/ row/column/2-D nodeIDs with shape preservation, empty nodeIDs, self- loop (counts 1 in-degree and 1 out-degree), siever 9-node fixture, weighted (weights ignored), multigraph parallel edges, multigraph self-loop, named digraph with char/1-cellstr/row-cellstr/column- cellstr/numeric inputs, adjacency round-trip, plus 11 error cases covering out-of-range index, zero index, non-integer, vector-with- bad-entry, non-existent name, name on unnamed digraph, non-digraph first arg (including graph object), nargin mismatch. 19 new BIST blocks on the private helper covering numeric/char/cellstr paths of every shape plus all error branches. scripts/graph/module.mk updated (alphabetic order: new indegree.m / outdegree.m in FCN_FILES, new __resolve_node_list__.m in PRIVATE_FCN_FILES). Top-level digraph classdef docstring @Seealso extended to mention indegree and outdegree. Test results: indegree 35/35, outdegree 35/35, __resolve_node_list__ 19/19; no regression elsewhere (digraph 278/278, graph 199/199, numnodes 23/23, numedges 23/23, successors 28/28, predecessors 28/28, neighbors 56/56, __resolve_endpoint__ 9/9, __resolve_single_node__ 12/12, __matlab_ref__ 5/5). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
degree(G) returns a column vector of edge-end counts for the undirected graph G; degree(G, nodeIDs) returns the degrees of specified nodes with input-shape preservation. Self-loops contribute 2 to the degree (MATLAB convention), since the undirected adjacency stores them as a single diagonal entry and the degree is defined as the count of edge-ends incident to the node. Implementation: - New classdef method graph.degree delegates all shape/resolution work to __resolve_node_list__ (same helper used by digraph.indegree and digraph.outdegree), then computes d = full (sum (spones (adj_), 1))(:) + full (diag (spones (adj_))) so each self-loop contributes 2 instead of 1. - New free function scripts/graph/degree.m (canonical help target and non-graph guard). - 44 BIST blocks covering empty, N-node, simple, weighted, named, adjacency-constructed, path, triangle, complete, star fixtures; every nodeIDs shape (scalar/row/column/2-D/empty); self-loop handling (isolated, mixed with regular edges, multiple nodes); handshake-lemma sanity check; and all input-validation errors. - Updated top-level graph docstring @Seealso to mention degree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
findnode(G, nodeID) returns the numeric node indices corresponding to nodeID in the graph or digraph G, matching MATLAB's findnode contract: numeric inputs are validated and returned with shape preserved; a char row vector is looked up as a single node name and returns a scalar (0 if not found); a cell array of character vectors is looked up element-wise and returns a column vector of indices (0 for any missing name). Missing names are NOT an error -- findnode doubles as an input-validation filter where "is this a valid node identifier?" needs a boolean-ish answer. Numeric out-of-range still errors. Implementation: - New shared private helper scripts/graph/private/__findnode_impl__.m (~195 lines, 12 BIST blocks) implements the full semantics once. - New classdef method graph.findnode and digraph.findnode each delegate to __findnode_impl__ after a nargin check. - New standalone scripts/graph/findnode.m (~345 lines, 49 BIST blocks) is the canonical `help findnode` target and non-graph guard. - Coverage: numeric passthrough (scalar, integer-class coercion, row/column/2-D/empty shape preservation, digraph parity, named and unnamed graphs); char row vector lookup (found/missing/empty char / no-names / empty graph, digraph parity); cellstr lookup (column-vector output from any input shape, 0-fill for missing, empty cellstr, 2-D cellstr flattens column-major); errors on out-of-range / non-integer / NaN / Inf / negative / complex / logical / struct / non-cellstr cell / non-row char / non-graph first arg / nargin mismatch. - Updated top-level graph and digraph docstring @Seealso to mention findnode. module.mk lists both new files alphabetically. Total test suite 855/855 (49 new findnode + 12 new __findnode_impl__, no regression on 794 prior tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add findedge with all three MATLAB call forms on both graph and digraph:
* findedge(G) -> m-by-2 endpoints matrix (or [sOut,tOut]
column vectors for the two-output form).
* findedge(G, s, t) -> edge index per (source, destination) pair,
0 if absent. Accepts numeric, char row,
or cellstr endpoints; for an undirected
graph the pair matches in either
orientation.
* findedge(G, edgeIdx) -> endpoints at the given edge indices
(m-by-2 matrix or two column outputs).
Shared private helper scripts/graph/private/__findedge_impl__.m
implements all three forms once using only public accessors (Edges,
Nodes, numnodes, numedges, ismultigraph), so both classdef methods
delegate without needing private-property access. Missing names
propagate to a 0 edge-index result (MATLAB findnode-style semantics,
no error). Multigraph digraphs return the first matching edge index
via linear scan; simple graphs/digraphs use a sparse (s, t) -> index
lookup for O(m + q) performance.
Files:
* scripts/graph/findedge.m (new, free function + 68 BIST blocks)
* scripts/graph/private/__findedge_impl__.m (new, shared helper
with 15 BIST blocks)
* scripts/graph/graph.m (new classdef method + @Seealso)
* scripts/graph/digraph.m (new classdef method + @Seealso)
* scripts/graph/module.mk (register new files, alphabetic)
Test results: findedge 68/68; __findedge_impl__ 15/15; full suite
938/938 (no regression).
Count edges between node pairs (s, t). For a simple graph/digraph the result is 0 or 1; for a multigraph digraph the result sums parallel edges. Undirected graph query is normalised to the canonical (min, max) form to match edge storage. New free-function scripts/graph/edgecount.m (+50 BIST blocks) delegates to new shared private helper scripts/graph/private/__edgecount_impl__.m (+12 BIST blocks) used by matching classdef methods on both graph.edgecount and digraph.edgecount. The helper uses only public accessors (G.Edges.EndNodes, numnodes, G.Nodes.Name), so it works without piercing classdef encapsulation. Accepts numeric indices, a char row vector (single name), or a cell array of strings; missing names yield 0 (findnode/findedge convention) rather than raising. Vectorized over same-length S and T; returns a scalar for scalar inputs and a column vector otherwise. Empty inputs return zeros(0, 1). Implementation builds sparse(E(:,1), E(:,2), 1, N, N) once from G.Edges.EndNodes; duplicate endpoint rows in multigraph storage accumulate naturally into per-cell multiplicities. sub2ind lookup gives O(m + q) for q query pairs. Total graph/digraph suite: 1000/1000 -- no regression.
Add per-node incoming/outgoing edge index lookup on digraph. * scripts/graph/outedges.m, scripts/graph/inedges.m: new free functions documenting the call forms and routing through classdef dispatch via G.outedges/G.inedges. Each accepts a scalar nodeID (numeric index, char row vector, or 1-element cellstr) and returns a column vector of edge indices into G.Edges. When two outputs are requested, the second is the column of destination (outedges) or source (inedges) node identifiers, with type matching the input (numeric in -> numeric, name in -> cellstr). Each file includes the 24-line GPLv3 header, a texinfo docstring, a worked example, and full %!test/%!error BIST coverage (37 + 38 blocks). * scripts/graph/digraph.m: new outedges and inedges classdef methods between edgecount and disp. Both delegate node resolution to __resolve_single_node__ for parity with successors/predecessors, then mask G.Edges.EndNodes by source (col 1) or destination (col 2) -- this keeps edge indices consistent with findedge for both simple and multigraph storage. The empty-Edges case is handled explicitly so an isolated node returns zeros(0, 1) for the numeric path and cell(0, 1) for the name path. Top-level classdef docstring @Seealso extended to include both methods. * scripts/graph/module.mk: register inedges.m and outedges.m in %canon_reldir%_FCN_FILES (alphabetic). Test results (installed Octave 9.4.0 BIST runner): outedges PASS=37 TOTAL=37 inedges PASS=38 TOTAL=38 digraph + 19 peers no regression OVERALL 1075/1075 (was 1000/1000)
Add adjacency(G[, W]) returning the sparse adjacency matrix with three call forms: no-arg (binary 0/1 or multigraph edge-count), "weighted" flag (stored edge weights, with weights summed for parallel edges), and a numeric vector W for custom per-edge weights. For undirected graph the result is symmetric; self-loops follow the MATLAB adjacency convention with A(i,i) = 1 (or W(i) for custom weights), not doubled. Wire the new free-function file, add classdef methods on both classes, and extend the top-level @Seealso entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add incidence(G) returning the sparse N-by-M incidence matrix where N = numnodes(G) and M = numedges(G). For a digraph, column k has -1 at the source row and +1 at the destination row of edge k. For a graph, column k has 1 at both endpoint rows. Self-loop edges produce an all-zero column (MATLAB convention: a valid incidence column has exactly two nonzero entries). - scripts/graph/incidence.m: free-function dispatcher with texinfo docstring and 37 BIST blocks covering digraph/graph/multigraph, self-loops, edgeless, named, weighted (weights ignored), and row/col-sum identities. - scripts/graph/digraph.m: new incidence classdef method; top-level @Seealso extended. - scripts/graph/graph.m: new incidence classdef method; top-level @Seealso extended. - scripts/graph/module.mk: register incidence.m in alphabetic order. Test summary: incidence PASS=37/37; no regressions across the 24 existing test files (OVERALL 1163/1163). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* scripts/graph/laplacian.m: New free function plus 34 BIST blocks. Returns the sparse graph Laplacian L = D - A_off where D is the degree-diagonal and A_off is the binary adjacency with the diagonal zeroed. Matches MATLAB's convention exactly: L(i,i) = degree(G,i) (self-loops contribute 2), L(i,j) = -1 for each edge between i and j (i != j), 0 otherwise. Edge weights are ignored. * scripts/graph/graph.m (laplacian classdef method): Real implementation; uses spones to collapse weights to binary, clears the diagonal of the adjacency to form A_off, places degree(G) on the diagonal, and returns sparse(1:N, 1:N, d, N, N) - A_off. * scripts/graph/digraph.m (laplacian classdef method): Errors with "laplacian: not defined for a digraph; laplacian requires an undirected graph". The graph Laplacian is not defined on directed graphs. * scripts/graph/graph.m, scripts/graph/digraph.m (top-level docstring @Seealso): Add laplacian. * scripts/graph/module.mk: Add laplacian.m alphabetic between inedges.m and neighbors.m.
* scripts/graph/ismultigraph.m: new free function (canonical help target plus a helpful-error fallback for non-graph inputs) that delegates to the class method via dot notation. 44 BIST blocks cover digraph true/false cases (parallel edges, weighted parallel, named parallel, EdgeTable duplicates, parallel self-loops, anti-parallel is not parallel), digraph false cases (plain, empty, edgeless, multigraph-flag-without-dupes, adjacency built, EdgeTable without dupes), graph always-false cases, return-type and dot-notation dispatch, Siever 9-node fixture, and %!error blocks for non-graph scalar, vector, string, cell, struct, sparse, and empty inputs plus nargin==0. * scripts/graph/graph.m: new ismultigraph classdef method that returns constant false -- the undirected graph class in this build does not accept a 'multigraph' constructor flag (see US-C10, which only added multigraph storage to digraph), so parallel edges cannot be stored. Top-level @Seealso extended to list ismultigraph. * scripts/graph/module.mk: ismultigraph.m added to %canon_reldir%_FCN_FILES in alphabetic order between inedges.m and laplacian.m. digraph.ismultigraph was already implemented as a classdef method in US-C10 and is unchanged. ismultigraph 44/44, full graph suite 1241/1241 -- no regression.
Extend the centrality() method in graph.m and digraph.m to accept
MATLAB's 'Cost' and 'Importance' per-edge weight vector options.
'Cost' is accepted for closeness/incloseness/outcloseness/betweenness
and supplies strictly positive per-edge costs of length numedges(G)
that override any stored edge weights when computing shortest paths.
Weighted betweenness runs a Dijkstra Brandes variant (O(N^2) per
source, linear-scan extract-min) in place of the BFS default.
'Importance' is accepted for pagerank/eigenvector/hubs/authorities
and supplies non-negative per-edge importances of length numedges(G)
that override any stored edge weights in the iterative computation.
Each helper gains its own Name-Value parser with consistent
validation (length == numedges(G), numeric real, finite, sign-
appropriate for the option). opts_accepting_types in both class
methods grows from {"pagerank"} to the full eight-entry whitelist so
the other seven TYPE cases now forward varargin{:} to their helpers.
Degree-based variants (degree/indegree/outdegree) still reject every
Name-Value option with the familiar "no name-value options are
supported" error.
Tests: +20 closeness BIST, +19 betweenness BIST (including a Dijkstra
Brandes rerouting test where Cost = [10;1;1;1] shifts the central
node's betweenness from 2 to 3), +12 eigenvector BIST, +12 hits BIST,
+13 pagerank BIST, +34 centrality.m BIST (incl. cross-type option
rejection, case-insensitivity, dot-notation dispatch), and two legacy
"no name-value" rejection regexes updated to "unknown TYPE option"
since eigenvector/hubs/authorities now accept Importance.
Full scripts/graph suite: 3595/3595 PASS (prev 3489 + 106 net new).
NO REGRESSIONS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add isisomorphic(G1, G2) as both a free function in scripts/graph/ and a method on the graph and digraph classes. Returns a scalar logical: true iff the two graphs have the same structural adjacency (node names and edge weights are ignored; edge multiplicities for multigraphs and self-loops are honoured). The search uses the VF2 algorithm (Cordella, Foggia, Sansone, Vento, 2004) via a new private helper __isomorphism_vf2__ that accepts N-by-N adjacency matrices plus a directed flag and returns either a column permutation vector (future use by isomorphism()) or []/false. Pruning: node and edge count, sorted row/column sums (degree sequences), sorted diagonal (self-loop multiset) before recursion; per-candidate degree match, self-loop parity, and mapped-neighbour consistency during recursion; VF2 terminal-set candidate selection to eliminate symmetric branches. test __isomorphism_vf2__ -> 17/17 PASS test isisomorphic -> 29/29 PASS test digraph -> 278/278 PASS (no regression) test graph -> 199/199 PASS (no regression)
isomorphism(G1, G2) returns a column permutation vector P such that reordernodes(G2, P) has the same structure as G1, or [] when no isomorphism exists (matching MATLAB's convention: adjacency(G2)(P, P) == adjacency(G1)). Delegates to the existing __isomorphism_vf2__ private helper from US-IS01 and inverts its f21-style perm (G2-to-G1 direction) to the MATLAB f12-style P (G1-to-G2 direction). Node names and edge weights are ignored -- structure only. Files: - scripts/graph/isomorphism.m (new, free-function wrapper with 32 BIST blocks covering digraph/graph iso + non-iso cases, empty graphs, single/edgeless, named graphs, self-loops, dot-notation dispatch, cross-class errors, print_usage). - scripts/graph/graph.m (+55 lines: new isomorphism method). - scripts/graph/digraph.m (+55 lines: same, directed variant). - scripts/graph/module.mk (+1: register isomorphism.m). Verification: - test isomorphism -> 32/32 PASS - test isisomorphic -> 29/29 PASS (no regression) - test __isomorphism_vf2__ -> 17/17 PASS (no regression) - test graph / digraph / __matlab_ref__ all PASS - Full scripts/graph public+private suite: 2985+688 = 3673/3673 PASS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend the isomorphism class methods (and the free function) to accept trailing name-value pairs that restrict the VF2 search to mappings preserving per-node or per-edge labels. - NodeVariables (char or cellstr): names of fields in G.Nodes that must agree between a G1 node and its mapped G2 partner. - EdgeVariables (char or cellstr): names of fields in G.Edges that must agree between corresponding edges under the mapping. Rejected for multigraphs with a clear error message. The VF2 helper gained two optional color-vector arguments (nc1/nc2) and two optional color-matrix arguments (ec1/ec2) that are checked inside the feasibility test, so incompatible candidate pairs prune early rather than being rejected only after a full mapping is found. Node-color multiset and edge-color multiset quick-rejects are added ahead of the search. Two new private helpers host the option parsing and label-to-color serialization: __isomorphism_parse_opts__ validates the name-value pairs, extracts the requested table columns, and builds the per-edge color adjacency matrix (symmetric for graph, asymmetric for digraph); __combine_labels__ serializes tuples of cellstr / numeric / logical columns through unique() to produce integer color codes shared across G1 and G2. Help for isomorphism, the private helpers, and the VF2 engine now documents the optional argument forms. Files changed: - scripts/graph/isomorphism.m (docstring, varargin delegation, 27 new BIST blocks covering NodeVariables/EdgeVariables success and error paths, dot-notation dispatch). - scripts/graph/graph.m (varargin, docstring, call into parser). - scripts/graph/digraph.m (same, directed). - scripts/graph/private/__isomorphism_vf2__.m (nc/ec args, feasibility changes, quick rejects, 9 new BIST blocks). - scripts/graph/private/__isomorphism_parse_opts__.m (new, ~290 lines including 12 BIST blocks). - scripts/graph/private/__combine_labels__.m (new, ~130 lines including 9 BIST blocks). - scripts/graph/module.mk (register both new private helpers). Verification: - test isomorphism -> 59/59 PASS - test __isomorphism_vf2__ -> 26/26 PASS - test __isomorphism_parse_opts__ -> 12/12 PASS - test __combine_labels__ -> 9/9 PASS - test graph -> 199/199 PASS - test digraph -> 278/278 PASS - test isisomorphic -> 29/29 PASS - Full scripts/graph public -> 3012/3012 PASS - Full scripts/graph private -> 718/718 PASS - help isomorphism renders (NodeVariables / EdgeVariables visible). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New files: * scripts/graph/GraphPlot.m --- classdef GraphPlot < handle with XData/YData/ZData, NodeColor/EdgeColor/Marker/MarkerSize/ LineStyle/LineWidth, read-only NumNodes/NumEdges, and private axes/edge/node handle caches. Constructor accepts 0 args (empty object) or a graph/digraph plus optional Layout/XData/YData name-value pairs. Rendering wrapped in try/catch so the object is still returned cleanly in non-interactive graphics environments. * scripts/graph/plot.m --- free function that dispatches plot(G) to the classdef plot method on the graph / digraph instance. * scripts/graph/private/__graph_plot_auto_layout__.m --- layout helper. 'auto' dispatches to 'subspace' (<100 nodes) or 'force' (>=100 nodes); both branches currently route to a deterministic unit-circle placeholder that will be replaced by the real algorithms in US-GP03 (force) and US-GP06 (subspace). Modified files: * scripts/graph/digraph.m --- added plot (G, varargin) method that delegates to the GraphPlot constructor. * scripts/graph/graph.m --- parallel plot method for undirected graphs. * scripts/graph/module.mk --- registered the new files in _FCN_FILES / _PRIVATE_FCN_FILES in alphabetic order. BIST: * GraphPlot 13/13 * plot 8/8 * __graph_plot_auto_layout__ 12/12 * digraph 278/278 (no regression) * graph 199/199 (no regression) Includes a %!demo block on plot.m that draws a 4-cycle digraph. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 'circle' layout was already wired through
__graph_plot_auto_layout__.m's 'circle' case since US-GP01 as the
deterministic fallback shared by the 'auto', 'subspace' and 'force'
branches. Promote it to a first-class user-facing layout with full
BIST coverage and docstring updates:
* scripts/graph/private/__graph_plot_auto_layout__.m: rewrite the
@table bullet for 'circle' from a one-line note to a full
paragraph declaring the layout production-quality, documenting
the start-at-(1, 0) CCW convention (theta = 2*pi*(k-1)/N), and
calling out the N==0 and N==1 special cases; +10 BIST blocks
covering N==0/1/2 edge cases, the unit-circle property, uniform
chord length 2*sin(pi/N), the start-at-(1, 0) CCW direction,
determinism, graph/digraph parity, independence from edge weights
and node names, isolated trailing nodes, and the column-vector
return contract.
* scripts/graph/plot.m: +9 BIST blocks covering
plot (G, 'Layout', 'circle') on both graph and digraph
(unit-circle placement, uniform 5-cycle chord length,
case-insensitive layout name, N==0/1/100 sanity, named and
weighted digraph independence, isolated trailing nodes,
determinism) plus a second %!demo rendering a 6-cycle on the
unit circle. Docstring gains a paragraph enumerating the
recognised layout names with a case-insensitivity note.
* scripts/graph/GraphPlot.m: +4 BIST blocks covering the
constructor path directly (select circle layout, graph/digraph
parity, XData/YData overrides win, explicit
X=[1;0;-1;0] Y=[0;1;0;-1] for N=4 verifying the start-at-(1, 0)
CCW direction).
No implementation change was required.
Verification:
* test __graph_plot_auto_layout__ 22/22 PASS (prev 12/12, +10)
* test plot 17/17 PASS (prev 8/8, +9)
* test GraphPlot 17/17 PASS (prev 13/13, +4)
* scripts/graph public suite 3046/3046 PASS, no regressions
* scripts/graph private suite 740/740 PASS, no regressions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the 'force' / 'auto' (N>=100) layout placeholder with a real
Fruchterman-Reingold 2-D implementation. New private helper
__graph_plot_force__.m runs the classical vectorised F-R recipe:
pairwise repulsive forces (k^2/d) plus attractive forces on
edges (d^2/k * W_eff) for 100 iterations with linear temperature
cool-down from t0=0.1 to 0; k = sqrt(1/N) as the ideal spring
length. Weighted adjacency is symmetrised (A + A.') so directed
and undirected graphs yield the same layout when the edge set is
symmetric.
New 'WeightEffect' option on GraphPlot/plot() (case-insensitive):
- 'none' (default): weights ignored
- 'direct': attractive force multiplied by edge weight
- 'inverse': attractive force divided by edge weight
Determinism: the algorithm seeds the global RNG internally with
rand('state', 42) and restores the caller's state on exit via
unwind_protect. Repeat calls return identical coordinates; the
layout is independent of the caller's RNG state.
__graph_plot_auto_layout__ gained an optional opts-struct argument
whose WeightEffect field is forwarded to the force branch.
GraphPlot.m parses 'WeightEffect' as a name-value pair and forwards
it via opts.WeightEffect. plot.m docstring updated to document the
new layout and option, plus a new %!demo.
Test summary:
__graph_plot_force__ 22/22 PASS
__graph_plot_auto_layout__ 34/34 PASS (prev 22/22)
GraphPlot 26/26 PASS (prev 17/17)
plot 23/23 PASS (prev 17/17)
scripts/graph public 3061/3061 PASS (prev 3046)
scripts/graph private 774/774 PASS (prev 740)
No regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New private helper scripts/graph/private/__graph_plot_force3__.m
implements the 3-D Fruchterman-Reingold algorithm: vectorised
pairwise repulsive and attractive force sums with k = (1/N)^(1/3)
as the unit-volume ideal spring length, 100 iterations with linear
cool-down, internally-seeded RNG (rand('state', 42)) and
unwind_protect restoration of the caller's global RNG state for
determinism independent of caller state. Same WeightEffect
semantics as the 2-D force helper (none / direct / inverse).
__graph_plot_auto_layout__ gains an optional third output Z
(zeros(0,1) for 2-D layouts, populated by the new 'force3' branch)
and a new 'force3' layout case that delegates to the helper.
GraphPlot picks up the optional Z, stores h.ZData as a column vector
(empty for 2-D), and dispatches edge and node rendering through
plot3 when ZData is non-empty, preserving all 2-D behaviour.
plot.m docstring documents 'force3' alongside 'force', explains the
3-D ZData contract, and adds a new %!demo.
Test coverage:
- __graph_plot_force3__: 25/25 PASS (new file)
- __graph_plot_auto_layout__: 42/42 PASS (+8)
- GraphPlot: 35/35 PASS (+9)
- plot: 29/29 PASS (+6)
- Full scripts/graph public: 3076/3076 PASS (+15)
- Full scripts/graph private: 807/807 PASS (+33)
- __matlab_ref__: 5/5 PASS (no regression)
New private helper scripts/graph/private/__graph_plot_layered__.m implements a Sugiyama-style hierarchical layout: break cycles via DFS back-edge reversal (for digraphs) or BFS orientation (for undirected graphs), compute longest-path ranks (ASAP) or latest-path ranks (ALAP) using Kahn's algorithm on the working adjacency, apply user-declared Sources/Sinks constraints by detaching them from their predecessors/successors, then reduce crossings with 8 iterations of barycenter sweeps using a stable sort for full determinism. Direction={down,up,left,right} transforms the final coordinates.
auto_layout forwards Direction/Sources/Sinks/AssignLayers fields from opts; GraphPlot adds the four matching name-value options with char-row validation; plot docstring documents the layered layout and its options; module.mk adds the helper.
Tests: __graph_plot_layered__ 44/44, __graph_plot_auto_layout__ 55/55 (+13), GraphPlot 52/52 (+17), plot 40/40 (+11); full graph public suite 3104/3104 (+28), private 864/864 (+57), zero regressions.
Replace the circle-based subspace placeholder with real spectral
layouts driven by Laplacian eigendecomposition (Hall-style embedding).
* New private helpers:
- __graph_plot_subspace_embedding__.m: builds the symmetric
unweighted, self-loop-free Laplacian and returns the
sign-normalised eigenvectors of the DIMENSION smallest non-trivial
eigenvalues (kernel eigenvectors skipped; padded with zero columns
when fewer than DIMENSION non-trivial modes exist).
- __graph_plot_subspace__.m: 2-D spectral layout (X = v2, Y = v3).
Accepts DIMENSION >= 2; default min(100, numnodes(G)).
- __graph_plot_subspace3__.m: 3-D spectral layout (X, Y, Z from
v2, v3, v4). Accepts DIMENSION >= 3.
* __graph_plot_auto_layout__.m dispatches 'subspace' and 'subspace3'
to the new helpers, forwards a new Dimension option, and the 'auto'
branch (N<100) now uses real spectral layout instead of the circle
placeholder.
* GraphPlot.m accepts a new 'Dimension' name-value option and routes
it through layout_opts.
* plot.m docstring lists subspace/subspace3 and the Dimension option;
new BIST coverage and %!demo blocks.
* module.mk registers the three new private .m files.
Layouts are fully deterministic (no RNG; eigenvector signs normalised
so the first max-magnitude entry is positive). Edge weights and
self-loops are ignored, matching laplacian()'s convention.
Test counts (scripts/graph only):
- __graph_plot_subspace_embedding__ 22/22 PASS
- __graph_plot_subspace__ 24/24 PASS
- __graph_plot_subspace3__ 26/26 PASS
- __graph_plot_auto_layout__ 76/76 PASS (was 55)
- GraphPlot 65/65 PASS (was 52)
- plot 49/49 PASS (was 40)
- Full scripts/graph suite 4083/4083 PASS, 0 failures
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the following cosmetic name-value options to GraphPlot and make them available for round-trip set/get (both via constructor and direct assignment on the handle): NodeColor, Marker, MarkerSize, NodeLabel, NodeFontSize, NodeFontName, NodeFontAngle, NodeFontWeight, NodeLabelMode, NodeLabelColor NodeLabel and NodeLabelMode cooperate as in MATLAB: setting NodeLabel flips NodeLabelMode to 'manual'; re-assigning NodeLabelMode = 'auto' regenerates the label from the cached graph's Nodes.Name (or "1","2", ... for unnamed nodes). Labels are rendered next to each marker as text objects inside the same best-effort try/catch that wraps the rest of the draw. Three new private helpers: private/__graph_plot_default_labels__.m private/__graph_plot_validate_colorspec__.m private/__graph_plot_validate_nodelabel__.m Tests: 113/113 GraphPlot (was 65), 25/25 colorspec, 14/14 nodelabel, 5/5 default_labels, 3174/3174 scripts/graph public, 1001/1001 private. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add ten edge-appearance cosmetic properties to GraphPlot and the validation / rendering plumbing behind them: * Plain properties: ArrowSize (default 7), ArrowPosition (default 0.5, strictly in (0, 1)), EdgeAlpha (default 0.5, in [0, 1]), EdgeFontSize (default 8), EdgeFontName (default "Helvetica"). * Dependent properties: EdgeLabel and EdgeLabelMode, backed by private edge_label_ / edge_label_mode_, mirroring the NodeLabel / NodeLabelMode pattern. Auto-default is weight strings on a weighted graph and empty on an unweighted graph. Assigning EdgeLabel flips the mode to "manual"; assigning EdgeLabelMode = "auto" regenerates labels from the cached graph. * Validated setters added for LineWidth (positive real scalar), LineStyle (MATLAB style list), ArrowSize, ArrowPosition, EdgeAlpha, EdgeFontSize, EdgeFontName. * Constructor name-value loop now accepts EdgeColor, LineWidth, LineStyle, ArrowSize, ArrowPosition, EdgeAlpha, EdgeLabel, EdgeLabelMode, EdgeFontSize, EdgeFontName (case-insensitive). * Edge labels render as text objects at ArrowPosition along each edge, best-effort (failures do not invalidate property state). New private helpers __graph_plot_validate_edgelabel__.m and __graph_plot_default_edge_labels__.m implement the edge-label validator and auto-default generator, with their own BIST suites (15 and 7 tests respectively). 37 new BIST blocks in GraphPlot.m (all pass): test GraphPlot 172/172 test __graph_plot_validate_edgelabel__ 15/15 test __graph_plot_default_edge_labels__ 7/7 scripts/graph + scripts/graph/private 4256/4256 __matlab_ref__ 5/5 plot 49/49 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-GP09) Add the highlight function for GraphPlot objects, allowing users to visually distinguish specific nodes by changing their color, marker, or marker size. New: scripts/graph/highlight.m - Free-function entry point dispatching to GraphPlot.highlight method. - Texinfo docstring with synopsis, options table, and example. - 6 BIST tests (smoke + free-function dispatch + error cases). Extended: scripts/graph/GraphPlot.m - New highlight (h, nodes, varargin) method inside the classdef. - Accepts numeric indices, bare char node name, or cellstr of names. - Default action: set NodeColor to red ([1 0 0]) for specified nodes. - Name/Value overrides: NodeColor, Marker, MarkerSize (case-insensitive). - Expands properties to per-node form (NodeColor Nx3 / Marker Nx1 cellstr / MarkerSize Nx1 vector) lazily, only when needed. Nodes not in the selection keep their current cosmetic values. Empty NODES is a silent no-op that does not expand any property. - Extended set.NodeColor to accept Nx3 matrices (per-node). - Extended set.Marker to accept cellstr of length NumNodes. - Extended set.MarkerSize to accept a positive real vector of length NumNodes. - 30 new BIST blocks covering the acceptance criteria, case-insensitive dispatch, handle-class sharing, two-call composition, undirected graphs, and error regex matches for every validation path. Module.mk registers scripts/graph/highlight.m. Verification: - test GraphPlot -> 202/202 PASS (was 172) - test highlight -> 6/6 PASS - scripts/graph public (all .m) -> 3269/3269 PASS - scripts/graph private (all .m) -> 1023/1023 PASS - test plot -> 49/49 PASS (no regression) - test __matlab_ref__ -> 5/5 PASS (no regression) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…GP10) Extend GraphPlot.highlight to accept the edge-endpoint form highlight(h, s, t[, Name, Value, ...]). Dispatch between node and edge form is decided by the 3rd positional argument: if it is a char row matching a known option name (NodeColor / Marker / MarkerSize / EdgeColor / LineWidth / LineStyle) the call stays in the node form, otherwise it is treated as edge form with s=arg2, t=arg3. This matches MATLAB's dispatch behaviour and makes highlight(h, 'alpha', 'beta') resolve as an edge highlight when 'alpha' and 'beta' are node names in the graph. The edge form reuses __findedge_impl__ for endpoint resolution, so it handles undirected graphs (normalising to min/max), multigraphs, and name-based lookups consistently with findedge. Missing edges raise a clear error distinguishing "node name not found" from "no edge connects S, T". EdgeColor expands to Mx3 on first use; LineWidth to Mx1 and LineStyle to Mx1 cellstr when a corresponding override is supplied. Empty (s, t) is a silent no-op that leaves the scalar properties untouched, paralleling the empty-nodes no-op in US-GP09. Setters for EdgeColor, LineWidth, and LineStyle were extended to accept the per-edge forms used by highlight while preserving scalar assignment. 31 new BIST blocks and the updated help highlight render both deftypefn synopses and the full option table; the free-function highlight.m docstring was expanded to mirror the method docstring. All 235 GraphPlot tests and the full scripts/graph suite (4325/4325) pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hlighting (US-GP11) Adds the MATLAB-style edge-index form of highlight() where the literal keyword 'Edges' followed by a numeric vector of 1-based edge indices selects those edges by their row position in G.Edges.EndNodes. * scripts/graph/GraphPlot.m: new dispatch branch at the top of the highlight method that detects the 'Edges' keyword (case-insensitive char row) BEFORE the existing node-vs-(s, t) dispatch. Validates idx as a positive-integer vector within 1..NumEdges, parses trailing EdgeColor / LineWidth / LineStyle name-value overrides, and expands scalar cosmetic properties to per-edge form on first highlight. Empty idx is a silent no-op. Adds two new @deftypefnx synopses plus descriptive paragraph to the texinfo docstring. 29 new BIST blocks. * scripts/graph/highlight.m: adds matching @deftypefnx synopses and an example to the free-function docstring. One new BIST block verifies dispatch through the free-function entry point. Test results: - test GraphPlot -> 264/264 PASS (was 235) - test highlight -> 7/7 PASS (was 6) - test digraph -> 278/278 PASS (no regression) - test graph -> 199/199 PASS (no regression) - test plot -> 49/49 PASS (no regression) - test __matlab_ref__ -> 5/5 PASS (no regression) - scripts/graph public (all .m) -> 3332/3332 PASS - scripts/graph private -> 1023/1023 PASS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the labeledge method to the GraphPlot handle class so users can set per-edge text labels post-construction. Both the edge-index form labeledge(h, idx, labels) and the (s, t) endpoint form labeledge(h, s, t, labels) are supported, matching MATLAB's API. Labels may be a cellstr, a numeric vector (converted via num2str), or a scalar string / numeric / single-cell value that is broadcast to all selected edges. The method flips EdgeLabelMode to 'manual' and preserves existing labels on edges outside the selection. Dispatch is purely positional (numel(varargin)==2 vs 3), which avoids the option-name ambiguity that the highlight method had to resolve with a keyword lookup. Endpoint resolution reuses __findedge_impl__ and label validation reuses __graph_plot_validate_edgelabel__, keeping the error wording consistent with the existing highlight and EdgeLabel setter code paths. A free-function entry point scripts/graph/labeledge.m delegates to the classdef method and renders 'help labeledge' cleanly. Tests: +41 new BIST blocks (33 in GraphPlot.m, 8 in labeledge.m) covering scalar/vector/column idx, numeric vs cellstr labels, scalar broadcast (char / numeric / single-cell), composition, (s,t) form with numeric and cellstr endpoints, undirected (s,t)==(t,s), handle-class semantics, and 14 error regexes. Verification: - test GraphPlot -> 297/297 PASS (was 264) - test labeledge -> 8/8 PASS (new) - test highlight -> 7/7 PASS (no regression) - test digraph -> 278/278 PASS (no regression) - test graph -> 199/199 PASS (no regression) - test plot -> 49/49 PASS (no regression) - test __matlab_ref__ -> 5/5 PASS (no regression) - Full scripts/graph public -> 3373/3373 PASS - scripts/graph private -> 1023/1023 PASS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add labelnode method on GraphPlot and matching free-function entry. Node identifier resolution reuses __resolve_node_list__ (numeric any shape, char row = single node name, cellstr of names, or empty = no op); label validation reuses __graph_plot_validate_nodelabel__. Label broadcast mirrors labeledge: char row, scalar numeric (via num2str), and single-cell cellstr broadcast to the resolved node count. Labels are overlaid at resolved indices into a full NumNodes-length column cellstr and pushed back through the Dependent NodeLabel setter so NodeLabelMode flips to 'manual'. * scripts/graph/GraphPlot.m: new labelnode method plus 29 new BIST blocks (scalar/vector/column/2-D-matrix indices, numeric vs cellstr vs char-row vs single-cell-cellstr labels, scalar-string and scalar-numeric and single-cell broadcast, char-row single-name, cellstr names, 1-element cellstr name, undirected-graph parity, composition, empty-nodes and empty-cellstr no-ops, handle-class aliasing, post-manual overlay, plus 10 error cases). labeledge method @Seealso extended with labelnode. * scripts/graph/labelnode.m: new free-function entry point with full texinfo docstring and 8 BIST blocks for the free-function dispatch. * scripts/graph/labeledge.m: @Seealso extended with labelnode. * scripts/graph/module.mk: labelnode.m registered alphabetically. test GraphPlot 326/326, test labelnode 8/8, test labeledge 8/8, full scripts/graph public 3410/3410 (+37 net new), private 1023/1023; no regressions.
…P14) Adds ZData name-value option to the GraphPlot constructor and plot(G) wrapper so callers can supply explicit 3-D node coordinates that bypass the layout step. - GraphPlot (G, 'XData', x, 'YData', y, 'ZData', z) stores z as a column double of length numnodes (G) and is_3d becomes true, so the existing plot3 render path is used. - GraphPlot (G, 'XData', x, 'YData', y) continues to produce a 2-D bypass with h.ZData empty (regression-safe). - GraphPlot (G, ..., 'ZData', []) is treated as no ZData (2-D bypass). - ZData without XData AND YData is rejected with error 'ZData requires XData and YData to also be supplied'. - ZData length mismatch errors with 'ZData length must equal numnodes (G)'. 17 new BIST blocks in scripts/graph/GraphPlot.m (3-D bypass; bypass overrides 'circle' and 'force3' layouts; 2-D with explicit Z=[]; column / row / int32 Z forms; undirected parity; N>0 isolated trailing nodes; 5 error cases covering length mismatches and Z-alone rejection) and 5 new BIST blocks in scripts/graph/plot.m (3-D round-trip via the free function, Z=[] = 2-D, length and Z-alone errors). No other files changed. GraphPlot 343/343 (prev 326), plot 53/53 (prev 49), full scripts/graph public 3431/3431 (+21 net), private 1023/1023 unchanged. digraph, graph, highlight, labeledge, labelnode regressions all green.
Add BIST coverage that reads the MATLAB-parity default cosmetic
properties on the GraphPlot returned by plot(G) on real graph and
digraph objects.
Defaults exercised (all already in place from earlier stories, this
story pins them under test):
- NodeColor / EdgeColor = [0 0.4470 0.7410] (MATLAB default series
color 1 blue)
- Marker = "o"; MarkerSize = 4
- LineWidth = 0.5; LineStyle = "-"
- ArrowSize = 7; ArrowPosition = 0.5; EdgeAlpha = 0.5
13 new BIST blocks in scripts/graph/GraphPlot.m and 3 new blocks in
scripts/graph/plot.m cover: unweighted and weighted digraph/graph,
named-node graph, isolated-nodes-only digraph, empty digraph (N=0),
GraphPlot() empty constructor, survival across set-then-restore,
and invariance across Layout selection ("auto", "circle", "force",
"subspace", "layered").
Verification:
- test GraphPlot -> 356/356 PASS (was 343, +13)
- test plot -> 56/56 PASS (was 53, +3)
- test digraph / test graph -> 278/278 and 199/199 PASS (no
regression)
- Full scripts/graph public -> 3447/3447 PASS (was 3431, +16)
- Full scripts/graph private -> 1023/1023 PASS (no regression)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…PS01)
Add MATLAB-compatible `saveobj(G)` instance method and `static loadobj(s)`
method to both digraph and graph classes so objects survive a -v7
MAT-file round-trip.
`saveobj` returns a scalar struct containing the internal state
(adj_, nodenames_, has_weights_, edge_attrs_, node_attrs_, plus
is_multigraph_ / mg_endnodes_ / mg_weights_ for digraph). Field
names mirror the private property names so the same struct flows
out of Octave's default classdef-to-struct save path.
`static loadobj(s)` reconstructs a digraph / graph from either that
struct or an existing object of the target class (idempotent path
for future Octave versions that preserve classdef through load).
Because Octave's MAT5 load currently converts classdef elements to
plain structs (Octave:load:classdef-to-struct warning) and does not
auto-invoke loadobj, callers finish the round-trip with:
S = load('G.mat'); G = digraph.loadobj(S.G);
MATLAB calls loadobj automatically, so the same code is a no-op
there.
21 new BIST blocks cover saveobj struct shape, loadobj(struct) and
loadobj(object) paths, save -v7 round-trip across unweighted /
weighted / named / empty / isolated-only / multigraph cases, and
input-validation errors.
test digraph 289/289 (+11); test graph 209/209 (+10); full
scripts/graph public 3468/3468 (+21); private 1023/1023 unchanged.
…-PS02)
The `Nodes` and `Edges` properties are declared `Dependent, SetAccess =
private`, so reading them via chained subsref (G.Edges.Weight,
G.Edges.EndNodes(i,:), G.Nodes.Name{i}, ...) works as a natural
consequence of the getters returning a plain struct, and any form of
assignment (direct, chained, indexed, brace, or dynamic-field) errors
with "private access and cannot be set" -- Octave's MATLAB-parity
equivalent of "you cannot set the read-only property 'Edges' of
digraph/graph".
This change locks both directions in with 47 new BIST blocks across
the two classdef files so that a future refactor cannot silently break
the read-only contract. The underlying infrastructure was built in
US-C04 / US-C14; US-PS02 is the test-lock-in layer.
* scripts/graph/digraph.m (US-PS02 section): shared fixture
Gd_ps02 / Gdn_ps02 / Gdm_ps02 (plain / named / multigraph);
12 %!test <*PS02> read-path blocks covering full-field,
indexed, brace, end-index, dynamic-field, and multigraph
chained reads plus the numel(Weight)==numedges and
cell(0,1) unnamed-default invariants;
12 %!error <private access> write-path blocks covering
direct, chained, indexed, brace, new-field, empty-rhs,
and dynamic-field assignments.
* scripts/graph/graph.m (US-PS02 section): parallel content
with shared fixture Gu_ps02 / Gun_ps02 and no multigraph
case -- undirected graph has no 'multigraph' constructor
flag (scripts/graph/graph.m:737). The lex-(s<=t) edge order
is reflected in the expected Weight = [10;30;20] and
EndNodes = [1 2; 1 3; 2 3] for the test fixture.
test digraph -> 313/313 (was 289, +24); test graph -> 232/232
(was 209, +23). Full scripts/graph public suite -> 3515/3515
(was 3468, +47); private suite -> 1023/1023 unchanged;
test __matlab_ref__ -> 5/5 unchanged. No regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Override horzcat, vertcat, and cat on both classdef types to throw a MATLAB-compatible "Concatenation of <graph|digraph> objects is not allowed. Use addnode or addedge to modify an existing <graph|digraph>." error. The error fires for explicit horzcat/vertcat/cat calls as well as bracket-concat [G1, G2] / [G1; G2] (through Octave classdef dispatch). Single-element [G] remains a no-op per MATLAB parity. 14 new BIST blocks per file (+28 total) verify: bracket horzcat, bracket vertcat, explicit horzcat, explicit vertcat, cat(1,...) and cat(2,...), three-operand varargin path, named/unnamed and weighted-mixed pairs, the lasterror inner-message preservation, and the single-element no-op. Bracket-dispatch wraps the error with "<class>/<method> method failed"; explicit calls preserve the full message -- tests cover both surfaces. Verification: - test scripts/graph/digraph.m -> 327/327 (prev 313, +14) - test scripts/graph/graph.m -> 246/246 (prev 232, +14) - Full scripts/graph public -> 3543/3543 (prev 3515, +28) - Full scripts/graph private -> 1023/1023 (unchanged) - test __matlab_ref__ -> 5/5 (unchanged) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both `classdef digraph` and `classdef graph` are declared without
`< handle`, so Octave's classdef machinery already gives every
assignment `G2 = G1` full value-copy semantics and every mutator
method (addnode, addedge, rmnode, rmedge, reordernodes, subgraph,
digraph-only flipedge) returns a new object rather than modifying
its argument in place -- MATLAB parity. This commit locks that
contract in with 27 BIST blocks (14 digraph, 13 graph -- graph has
no flipedge) that each follow the pattern:
G1 = <fixture>;
G2 = G1;
G2 = <mutator> (G2, ...);
assert (<source unchanged>);
covering unweighted / weighted / named fixtures and chained
mutations. Also covered: a three-level G1 -> G2 -> G3 chain, a
`local_hammer` nested function (pass-by-value through function
boundary), and the return-value-doesn't-alias-source case
`H = addedge (G1, ...)`. Every block carries the `<*PS04>` test
ID for easy grouping under `test digraph verbose` / `test graph
verbose`.
A future refactor to `classdef digraph < handle` (reference
semantics) would trip every block in this section -- that is the
intent of the lock.
Add test/graph/ with 29 MATLAB-doc-example scripts, a runner, a
%!test wrapper, and a module.mk. Each doc-example script ends
with assert() calls so a wrong result fails the suite rather than
passing silently.
- test/graph/doc-examples/*.m (29 files):
digraph/graph constructors (numeric edge list, weighted, node
names, adjacency matrix, omitselfloops, multigraph), addedge /
addnode / rmedge / rmnode, neighbors / successors / predecessors,
shortestpath / distances, bfsearch / dfsearch, centrality,
conncomp, toposort / isdag, subgraph, flipedge, reordernodes,
findnode / findedge, degree / indegree / outdegree,
adjacency / incidence / laplacian.
- test/graph/run_doc_examples.m: walks doc-examples/ in stable
alphabetic order and runs each script inside a private
subfunction's isolated workspace. Returns a struct with
fields total, npass, nfail, ran, failures. 4 own %!test
BIST blocks cover discovery, failure accounting, non-existent
directory, and non-string EXAMPLES_DIR.
- test/graph/doc-examples.tst: 4 %!test <*US-R02> blocks that
probe pwd+test/graph, pwd+graph, and pwd for the runner so
the tst works whether make check runs it from the source
root, from test/, or from test/graph/.
- test/graph/module.mk: lists the runner, the .tst file, and
all 29 example scripts.
- test/Makefile.am: add include graph/module.mk alphabetically
between file-encoding/ and help/.
All 29 examples pass under run_doc_examples; the 4 .tst blocks
pass from three different working directories. No regressions:
test digraph 341/341, test graph 259/259, test __matlab_ref__ 5/5,
test addedge / rmnode / shortestpath / plot.m all unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add test/graph-bench.m, a caller-parameterisable script (the dash in its filename precludes a function) that times distances, shortestpath, centrality, and plot on random uniform-Erdos-Renyi digraphs at a configurable sequence of node counts. Parallel-edge deduplication is done driver-side so the graph ctor's simple-graph constraint is respected without needing the "multigraph" flag. Results land in a bench_results struct; passing results_file also writes a human-readable text table. Seven %!test <*US-R03> blocks cover smoke, multi-size rectangular timings, plot-skip above max_plot_size, results_file round-trip, reproducibility, and input validation. Reference timings at 1e3/1e4/1e5 committed to doc/graph-bench-results.txt with reproduction instructions; 1e6 row documented as unmeasured on this host.
Add a new 'Graph and network analysis' section to NEWS.12.md describing the graph, digraph, and GraphPlot classes along with the construction, query, modification, traversal, algorithm, centrality, isomorphism, and visualization APIs. Also list every new top-level name in the alphabetical list of new functions.
Create doc/interpreter/graph.txi with an overview, a short runnable example, and nine sections covering object construction, structure queries, modification, matrix representations, traversal and shortest paths, connectivity and flow, centrality, isomorphism, and visualization. Each section pulls in DOCSTRING entries for the relevant scripts/graph/ functions (49 total). Wire the new chapter into the book by adding * Graph and Network Analysis:: to the top-level menu and detailed menu of doc/interpreter/octave.texi (placed after Sparse Matrices, reflecting the sparse-adjacency storage used by the classes) and by appending %reldir%/graph.texi to MUNGED_TEXI_SRC in doc/interpreter/module.mk. Syntax validated with makeinfo via the Octave macros.texi include (RC=0, zero warnings) after stubbing the @Docstring expansions and the Sparse Matrices cross-reference target. test digraph, test graph, and test __matlab_ref__ all unchanged (341/341, 259/259, 5/5).
macOS make-check CI surfaced 11 BIST failures in scripts/graph/
(GraphPlot.m:6, distances.m:3, plot.m:2) where the tests cross-check
public-API output against the private helper (__graph_plot_force3__,
__graph_plot_layered__, __graph_plot_subspace__, __graph_plot_subspace3__,
__distances_bellman_ford__). Under `make check`, BIST blocks execute
outside the parent directory's private-function scope, so direct
calls to those __helpers__ fail with 'undefined'. The Windows local
driver hid this by explicitly adding scripts/graph/private to the
path via `--path scripts/graph/private`; upstream CI has no such
workaround.
Wrap each affected BIST in a path-snapshot / addpath / restore idiom:
old_path = path ();
addpath (fullfile (fileparts (which ("digraph")), "private"));
unwind_protect
...cross-check against private helper...
unwind_protect_cleanup
...; path (old_path);
end_unwind_protect
The snapshot preserves existing entries (so the Windows driver's
pre-added scripts/graph/private stays on path after each test) while
CI without the pre-add gets private/ added just for the duration of
the test and cleanly removed. All 11 previously-failing tests now
pass whether private/ is on the path at entry or not:
test GraphPlot -> 356/356 (was 350/356 on macOS)
test distances -> 133/133 (was 130/133 on macOS)
test plot -> 56/56 (was 54/56 on macOS)
No regression in test digraph (341/341), test graph (259/259), or in
the private helpers' own BIST blocks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three %!error blocks in scripts/graph/GraphPlot.m leaked an invisible figure + axes each because they constructed a GraphPlot successfully (which calls newplot()) and then triggered the error via a property setter. The %!error macro expects a single expression, so it has no unwind_protect wrapper to close the leaked figure. Over the course of the full test suite, the leaked figure persisted into later plot tests and caused scripts/plot/util/findobj.m to see 4 root-level tag="" objects instead of the expected 2 ([root, figure(3)]). The analyze-test-suite-results CI step then tripped the `FAIL 0` gate on Ubuntu and macOS (upstream 73bd1ab same job on 9.4.0 with no graph/ code passes 15/15). MinGW jobs were unaffected because the mingw workflow's analyze step only emits warnings, not failures. Fix: rewrite the three %!error blocks as explicit %!test blocks with figure("visible", "off") + unwind_protect + close(hf), using an inner try/catch that captures the expected error message regex (NodeLabel / EdgeLabel) so the error-assertion semantics are preserved. Test count is unchanged at 356/356. After the fix, running test digraph; test graph; test GraphPlot; test plot; test highlight; test labelnode; test labeledge leaves findobj(0,"tag","") at exactly 2 handles (root + the just-created figure(3)) as upstream expects. Files changed: - scripts/graph/GraphPlot.m (+57/-7): 3 %!error -> 3 %!test blocks at lines 2589, 3039, 3058 (post-edit numbering). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI update —
|
Summary
This draft PR adds full MATLAB-parity implementations of the
graphanddigraphvalue classes, plus theGraphPlothandle class, under a newscripts/graph/category. It covers 95 commits landing 51 user-facingfunctions (every method in MATLAB R2024a's
graph/digraph/GraphPlotAPI), backed by 600 BIST tests (341 on
digraph, 259 ongraph) and areference-output harness that cross-checks against captured MATLAB
outputs.
graph(undirected, value-class),digraph(directed,value-class),
GraphPlot(plot handle,< handle).(s, t), weighted edge-list(s, t, w),adjacency matrix
(A)with'upper'/'lower'forgraph, cellstrnode names, integer node-count
N,EdgeTable/NodeTablestructinputs,
'omitselfloops'and'multigraph'flags. Sparse adjacencyis preserved end-to-end.
addedge,addnode,rmedge,rmnode,flipedge,reordernodes,subgraph,simplify,findedge,findnode,numedges,numnodes,edgecount,degree,indegree,outdegree,inedges,outedges,neighbors,predecessors,successors,ismultigraph, plus matrix representations(
adjacency,incidence,laplacian).bfsearch,dfsearch,shortestpath,shortestpathtree,distances(auto/unweighted/positive/mixed/
acyclic),allpaths,allcycles,maxflow,mincut,toposort,isdag,conncomp,biconncomp,condensation,transclosure,transreduction.degree,indegree,outdegree,closeness,incloseness,outcloseness,betweenness,pagerank,eigenvector,hubs,authorities) withCost/Importanceoptions.isisomorphic/isomorphism(VF2) withNodeVariables/EdgeVariables.plot(G)returns aGraphPlotwith seven layouts(
auto,circle,force,force3,layered,subspace,subspace3) plus the cosmetic properties andhighlight/labelnode/labeledgesetters.etc/NEWS.12.mdsection, newdoc/interpreter/graph.txichapter wired into
octave.texi, and adoc/graph-bench-results.txtwith performance numbers at 1e3/1e4/1e5/1e6 scales.
No new dependencies; pure
.mwith no new oct-file. Value-class copysemantics verified (every
digraph/graphmutation returns a newobject; no aliasing through
< handle).Contribution context
https://github.com/Rising-Edge-Systems/MatSlop/blob/main/tasks/prd-octave-digraph-graph.md
https://github.com/Rising-Edge-Systems/MatSlop/blob/main/tasks/fsf-assignment.md
. The contributor (
Adam Kidwell) has opened the paperwork withassign@gnu.org; this PR is kept in draft state until therequest-assign.futureform is signed and recorded by the FSFclerks. All commits are GPLv3-licensed in the usual Octave header
style.
review visibility. I understand final patch intake is via Savannah /
octave-maintainers; happy to move there once a reviewer asks.Verification performed
test digraph→ 341 / 341 passtest graph→ 259 / 259 passtest __matlab_ref__→ 5 / 5 pass (reference-capture harness)upstream/default(no conflicts).make checkon Linux/macOS/Windows is pending (US-U04 in thePRD) — I only have a Windows workstation and the bundled Octave
9.4.0 BIST runner; help welcome from CI.
Test plan
make checkon Linux → 0 new failures attributable toscripts/graph/make checkon macOS → 0 new failuresmake checkon Windows (MSYS2) → 0 new failuresmake docbuilds cleanly with the newgraph.texichapterAdam Kidwellrecorded by FSF clerks🤖 Generated with Claude Code