Skip to content

WIP: Add an is_chordal algorithm#434

Draft
Luis-Varona wants to merge 8 commits intoJuliaGraphs:masterfrom
Luis-Varona:431-is_chordal-algorithm
Draft

WIP: Add an is_chordal algorithm#434
Luis-Varona wants to merge 8 commits intoJuliaGraphs:masterfrom
Luis-Varona:431-is_chordal-algorithm

Conversation

@Luis-Varona
Copy link

We implement Tarjan and Yannakakis (1984)'s MCS algorithm, taking inspiration from the existing NetworkX implementation. Everything is done except for example doctests in src/chordality.jl and unit tests in test/chordality.jl. (This PR is part of a new suite of algorithms outlined in issue #431.)

@codecov
Copy link

codecov bot commented Jun 2, 2025

Codecov Report

❌ Patch coverage is 96.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.28%. Comparing base (89dabeb) to head (10c885b).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
src/chordality.jl 96.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #434      +/-   ##
==========================================
- Coverage   97.29%   97.28%   -0.01%     
==========================================
  Files         123      127       +4     
  Lines        7421     7699     +278     
==========================================
+ Hits         7220     7490     +270     
- Misses        201      209       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@simonschoelly
Copy link
Member

Do you by any change know where I could find a public accessible version of that paper? My usual methods did not help.

@Krastanov
Copy link
Member

out.pdf

The paper is attached here, courtesy of the UMass library.

@Luis-Varona
Copy link
Author

out.pdf

The paper is attached here, courtesy of the UMass library.

Wonderful, @Krastanov, thanks!

@Luis-Varona
Copy link
Author

@simonschoelly, just wanted to let you know I certainly haven't abandoned this PR. I was busy with work this past week and I'm at a conference this weekend, but I'll get back to it shortly. I've looked over your comments and it shouldn't be many more changes at all anyway, hehe.

@Luis-Varona
Copy link
Author

@simonschoelly, just changed the input type from AbstractSimpleGraph to AbstractGraph. Anything else you'd like me to change before I get around to adding tests?

@Luis-Varona
Copy link
Author

Wait—I just remembered your comment about induced subgraphs. I'll think about that and get back to you I was largely just porting over networkx's implementation, which does do this, but it probably really isn't optimal.

@simonschoelly
Copy link
Member

@simonschoelly, just wanted to let you know I certainly haven't abandoned this PR. I was busy with work this past week and I'm at a conference this weekend, but I'll get back to it shortly. I've looked over your comments and it shouldn't be many more changes at all anyway, hehe.

Don't worry - we are quite slow with reviewing PRs here :D

@Luis-Varona
Copy link
Author

@simonschoelly @Krastanov I've fixed the issue regarding unnecessary allocations with induced_subgraph, instead having the _induces_clique helper take in a vertex subset and, in the context of the original graph, confirm that all possible edges exist. @simonschoelly, is there anything else I should change before I start on unit tests and docstring examples?

@Luis-Varona
Copy link
Author

Hi @simonschoelly, just following up. Is everything good enough for me to conclude my work on the actual logic and start on the docstring and unit tests? 🙂

@Luis-Varona
Copy link
Author

What do you think, @Krastanov? 🙂

@Krastanov
Copy link
Member

Thanks, @Luis-Varona ! No need to wait for anything else to be finished before adding tests on my end. I actually frequently refuse to review PRs before the tests are added, as I judge quality first by the tests and documentation.

I am adding some quality of life improvements to IGraphs.jl that should make it easy to test against their implementation as well. I will post it here shortly.

@Krastanov
Copy link
Member

Just to clarify: while I might have a particular style of review, that style is not shared by everyone, and please do not hesitate to request me approaching this differently.

@Krastanov
Copy link
Member

IGraphs.jl just got a new release in which you can use the following in order to test your implementation against the ones in the igraph C library https://igraph.org/c/html/latest/igraph-Structural.html#igraph_is_chordal

the_graph = random_regular_graph(10,5)
LibIGraph.is_chordal(IGraph(the_graph), IGNull(), IGNull(), IGNull(), IGNull())

I frequently use random sampling over appropriate ensembles as a way to test that two implementations give the same result. This can be useful here as well.

It can also be fun to compare performance (just make sure to not count the time to convert the graph from one format to the other).

@simonschoelly
Copy link
Member

There is also https://github.com/wangjie212/ChordalGraph

@simonschoelly
Copy link
Member

You resolved that conversation but might have overlooked what I wrote there about using the IsDirected trait.

@Luis-Varona
Copy link
Author

Luis-Varona commented Jun 30, 2025

Thanks for the info, @Krastanov!

You resolved that conversation but might have overlooked what I wrote there about using the IsDirected trait.

So I did, @simonschoelly. Sorry—I must've overlooked that. I expect that sort of fix shouldn't take long, anyway, so I'll try to take a crack at it later this week.

I suppose regardless of the internal workings of our version of is_chordal, I can also start right away on the test suite. Testing against only one of the two libraries suggested above (i.e., ChordalGraphs.jl or IGraphs.jl, not both) should be sufficient, right?

Edit: Certainly, benchmarking our implementation against both of the above would be useful, but I'm thinking I'll leave that to the benchmark/ folder and just use one or the other as a point of comparison in the actual test suite in test/.

@Krastanov
Copy link
Member

Yes, no need to test against multiple other libraries. Your own correctness tests are probably most important -- testing against another library with a bunch of random samples just provides a bit more certainty that we are not missing anything.

@Luis-Varona
Copy link
Author

Hi @Krastanov and @simonschoelly, sorry for the long silence—I plan to return to this issue sometime next week. IIRC, all that's left is just a test suite, and maybe a docs fix. 🙂

@Krastanov
Copy link
Member

Thanks @Luis-Varona

@samuelsonric
Copy link

samuelsonric commented Oct 15, 2025

@Luis-Varona @Krastanov @simonschoelly I have an extremely performant implementation of this function here, if that is of any interest. In particular, the MCS algorithm is best implemented using a bucket queue: see here.

@Krastanov
Copy link
Member

@samuelsonric , thanks for the heads up! It would be great to have access to both implementations from Graphs.jl -- this also makes testing much easier as we can just do random tests against each other.

If you or anyone else has the bandwidth, my suggestion would be to do

is_chordal(graph) which dispatches to is_cordal(graph, alg::GraphsMCSAlg) which is the algorithm implemented here and then in CliqueTrees.jl import Graphs; Graphs.is_cordal(graph, alg::CliqueTreesMCSAlg). The reason I am suggesting the extension happening in CliqueTrees is because CliqueTrees already depends on Graphs. The reason I am suggesting keeping both options is because it is valuable to keep the Graphs dependencies as minimal as possible.

As for is_chordal vs ischordal, it seems we mostly use the underscore notation for public functions:

image

@samuelsonric
Copy link

samuelsonric commented Oct 15, 2025

@Krastanov,

I am happy to do this. I can probably implement is_cograph as well.

@Krastanov
Copy link
Member

Thanks, Richard!

@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 2026

Benchmark Results (Julia v1)

Time benchmarks
master 10c885b... master / 10c885b...
centrality/digraphs/betweenness_centrality 17.1 ± 0.32 ms 17 ± 0.25 ms 1.01 ± 0.024
centrality/digraphs/closeness_centrality 12.2 ± 0.32 ms 12.2 ± 0.24 ms 0.995 ± 0.033
centrality/digraphs/degree_centrality 1.93 ± 0.18 μs 1.93 ± 0.17 μs 0.999 ± 0.13
centrality/digraphs/katz_centrality 0.882 ± 0.048 ms 0.866 ± 0.042 ms 1.02 ± 0.075
centrality/digraphs/pagerank 0.0367 ± 0.0047 ms 0.0363 ± 0.00079 ms 1.01 ± 0.13
centrality/graphs/betweenness_centrality 29.6 ± 1.7 ms 29.1 ± 1.7 ms 1.02 ± 0.085
centrality/graphs/closeness_centrality 22.4 ± 0.49 ms 22 ± 0.52 ms 1.02 ± 0.032
centrality/graphs/degree_centrality 1.51 ± 0.18 μs 1.49 ± 0.16 μs 1.01 ± 0.16
centrality/graphs/katz_centrality 1.04 ± 0.044 ms 1.03 ± 0.049 ms 1.01 ± 0.064
connectivity/digraphs/strongly_connected_components 0.0438 ± 0.0024 ms 0.0432 ± 0.0014 ms 1.01 ± 0.064
connectivity/graphs/connected_components 24.3 ± 1.1 μs 24.1 ± 0.79 μs 1.01 ± 0.058
core/edges/digraphs 7.74 ± 0.07 μs 7.08 ± 0.01 μs 1.09 ± 0.01
core/edges/graphs 17 ± 0.09 μs 17.3 ± 0.07 μs 0.98 ± 0.0065
core/has_edge/digraphs 5.34 ± 0.52 μs 5.26 ± 0.36 μs 1.02 ± 0.12
core/has_edge/graphs 5.76 ± 0.61 μs 5.51 ± 0.39 μs 1.05 ± 0.13
core/nv/digraphs 0.361 ± 0.01 μs 0.361 ± 0.01 μs 1 ± 0.039
core/nv/graphs 0.381 ± 0.01 μs 0.381 ± 0.011 μs 1 ± 0.039
edges/fille 8.56 ± 1.1 μs 8.28 ± 0.97 μs 1.03 ± 0.18
edges/fillp 7.2 ± 4 μs 6.03 ± 3.6 μs 1.19 ± 0.97
edges/tsume 2.6 ± 0.019 μs 2.48 ± 0.02 μs 1.05 ± 0.011
edges/tsump 2.53 ± 0.04 μs 2.5 ± 0.03 μs 1.01 ± 0.02
insertions/SG(n,e) Generation 25.6 ± 4.2 ms 25.6 ± 4.4 ms 1 ± 0.24
parallel/egonet/twohop 0.347 ± 0.012 s 0.318 ± 0.007 s 1.09 ± 0.045
parallel/egonet/vertexfunction 2.95 ± 0.47 ms 2.57 ± 0.16 ms 1.15 ± 0.2
serial/egonet/twohop 0.356 ± 0.017 s 0.317 ± 0.0058 s 1.12 ± 0.058
serial/egonet/vertexfunction 3.37 ± 0.41 ms 2.52 ± 0.19 ms 1.34 ± 0.19
traversals/digraphs/bfs_tree 0.0499 ± 0.0098 ms 0.0497 ± 0.0083 ms 1 ± 0.26
traversals/digraphs/dfs_tree 0.0645 ± 0.011 ms 0.0639 ± 0.0091 ms 1.01 ± 0.22
traversals/graphs/bfs_tree 0.0536 ± 0.003 ms 0.0532 ± 0.0023 ms 1.01 ± 0.071
traversals/graphs/dfs_tree 0.0662 ± 0.005 ms 0.0659 ± 0.0034 ms 1 ± 0.092
time_to_load 0.564 ± 0.0012 s 0.554 ± 0.0037 s 1.02 ± 0.0072
Memory benchmarks
master 10c885b... master / 10c885b...
centrality/digraphs/betweenness_centrality 0.29 M allocs: 24 MB 0.29 M allocs: 24 MB 1
centrality/digraphs/closeness_centrality 18.6 k allocs: 14.5 MB 18.6 k allocs: 14.5 MB 1
centrality/digraphs/degree_centrality 8 allocs: 5.01 kB 8 allocs: 5.01 kB 1
centrality/digraphs/katz_centrality 2.63 k allocs: 2.83 MB 2.63 k allocs: 2.83 MB 1
centrality/digraphs/pagerank 21 allocs: 14.9 kB 21 allocs: 14.9 kB 1
centrality/graphs/betweenness_centrality 0.545 M allocs: 0.0313 GB 0.545 M allocs: 0.0313 GB 1
centrality/graphs/closeness_centrality 19.3 k allocs: 14 MB 19.3 k allocs: 14 MB 1
centrality/graphs/degree_centrality 10 allocs: 5.43 kB 10 allocs: 5.43 kB 1
centrality/graphs/katz_centrality 2.96 k allocs: 3.1 MB 2.96 k allocs: 3.1 MB 1
connectivity/digraphs/strongly_connected_components 1.05 k allocs: 0.075 MB 1.05 k allocs: 0.075 MB 1
connectivity/graphs/connected_components 0.061 k allocs: 22.5 kB 0.061 k allocs: 22.5 kB 1
core/edges/digraphs 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
core/edges/graphs 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
core/has_edge/digraphs 20 allocs: 12.6 kB 20 allocs: 12.6 kB 1
core/has_edge/graphs 28 allocs: 13.8 kB 28 allocs: 13.8 kB 1
core/nv/digraphs 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
core/nv/graphs 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
edges/fille 3 allocs: 0.153 MB 3 allocs: 0.153 MB 1
edges/fillp 3 allocs: 0.153 MB 3 allocs: 0.153 MB 1
edges/tsume 0 allocs: 0 B 0 allocs: 0 B
edges/tsump 0 allocs: 0 B 0 allocs: 0 B
insertions/SG(n,e) Generation 0.0465 M allocs: 10.9 MB 0.0465 M allocs: 10.9 MB 1
parallel/egonet/twohop 10 allocs: 0.0768 MB 10 allocs: 0.0768 MB 1
parallel/egonet/vertexfunction 10 allocs: 0.0768 MB 10 allocs: 0.0768 MB 1
serial/egonet/twohop 3 allocs: 0.0764 MB 3 allocs: 0.0764 MB 1
serial/egonet/vertexfunction 3 allocs: 0.0764 MB 3 allocs: 0.0764 MB 1
traversals/digraphs/bfs_tree 2.34 k allocs: 0.113 MB 2.34 k allocs: 0.113 MB 1
traversals/digraphs/dfs_tree 2.44 k allocs: 0.118 MB 2.44 k allocs: 0.118 MB 1
traversals/graphs/bfs_tree 2.52 k allocs: 0.121 MB 2.52 k allocs: 0.121 MB 1
traversals/graphs/dfs_tree 2.63 k allocs: 0.127 MB 2.63 k allocs: 0.127 MB 1
time_to_load 0.145 k allocs: 11 kB 0.145 k allocs: 11 kB 1

@Luis-Varona Luis-Varona force-pushed the 431-is_chordal-algorithm branch from d2eb7cc to fdcc770 Compare February 24, 2026 16:48
We implement Tarjan and Yannakakis (1984)'s MCS algorithm, taking inspiration from the existing NetworkX implementation. Everything is done except for example doctests in src/chordality.jl and unit tests in test/chordality.jl. (This PR is part of a new suite of algorithms outlined in issue JuliaGraphs#431.)
Originally, we restricted the input type for is_chordal to AbstractSimpleGraph to rule out parallel edges, but some other graph types we would like to support (such as SimpleWeightedGraph) are of the more general AbstractGraph type. It turns out the current AbstractGraph interface aleady does not support parallel edges, so there is no worry here--it is already stated in the Implementation Notes anyway that is_chordal should not take in graphs with parallel edges as input.
Luis-Varona and others added 2 commits February 24, 2026 12:49
Originally mirroring the networkx implementation, we created a new AbstractGraph object every time we tested subsequent neighbors in the potential PEO with the induced_subgraph function. This commit makes our implementation more performant by simply taking the vertex (sub)set and checking to see if all pairs are adjacent via iteration.

Additionally, we change inconsistent naming in certain parts ('node' vs 'vertex'), changing everything to 'vertex' where relevant.
@Luis-Varona Luis-Varona force-pushed the 431-is_chordal-algorithm branch from fdcc770 to 4e46aab Compare February 24, 2026 16:49
@Luis-Varona
Copy link
Author

Hi @simonschoelly and @Krastanov - I'm really sorry for the long absence. I've just gotten back to work on this PR. (Because it kept getting messy from me merging back in from master, I cherry-picked my old commits onto a french branch from master.)

@simonschoelly, I incorporated (in the latest commit) your trait dispatch suggestions; hopefully it aligns with what you had in mind. I've also added docstring examples, @Krastanov.

The next step, I suppose, is to update the test suite, and add to the documentation and changelog. (Both of those things I am to do within the day.) As for @samuelsonric's performant implementation, I'll certainly take a look.

We create docs/src/algorithms/chordality.md, adding it to the Algorithms API section of docs/make.jl.
@Krastanov
Copy link
Member

Thanks, @Luis-Varona . Indeed the main next step would be tests, including correctness tests against other libraries. No worries about the delay, we probably would not have had an active volunteer to help with review at that time anyway.

@Luis-Varona
Copy link
Author

Got it @Krastanov! Do you suggest including dispatch (re: @samuelsonric's implementation) in this PR or in a future one after the basic functionality has been finalized here?

@Krastanov
Copy link
Member

Your PR is the first one to introduce an implementation of is_chordal that is free of other dependencies. We can think of this as the default implementation. No need to include dispatch on other implementations -- and if we are to introduce such dispatch, it would be defined in whatever other library implements it, something like is_chordal(..., ::OtherLibraryAlgorithm) defined in the body of OtherLibrary. So no need to do anything extra here (unless you are talking about some other dispatch-related setup)

We add IGraphs as a test dependency (to the nested Project.toml in the test folder, and import it as well in runtests) for our soon-to-be-completed chordality test suite.
The GenericGraph type used in the unit tests returns a generator object when neighbors is called on it. Originally, we computed the subsequent_neighbors variable via intersect(neighbors(g, v), numbered), but intersect could not infer the element type of a generator object and fell back to Vector{Any}. This caused a MethodError when passing the result to _induces_clique, so we replaced the intersect call with a filter call.
Short-circuit one-liners (e.g., 'A || do B') are expanded into 'if' blocks (similarly, 'if !A; do B; end'), and the cardinality closure in '_max_cardinality_vertex' is inlined as a lambda function.
@Luis-Varona Luis-Varona force-pushed the 431-is_chordal-algorithm branch from 6708985 to 10c885b Compare February 28, 2026 04:42
@Luis-Varona
Copy link
Author

Luis-Varona commented Feb 28, 2026

@Krastanov, I've finished the test suite locally and an just adding some comments for clarity before pushing. I'll probably finish it either before sleeping or sometime tomorrow. 🙂

(And of course, I'll update the CHANGELOG as well once I'm done with that.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants