Skip to content

Add graph edit distance solver#37

Draft
jhandwe wants to merge 37 commits into
JuliaGraphs:mainfrom
Lab-CA-SS26:graph_edit_distance
Draft

Add graph edit distance solver#37
jhandwe wants to merge 37 commits into
JuliaGraphs:mainfrom
Lab-CA-SS26:graph_edit_distance

Conversation

@jhandwe

@jhandwe jhandwe commented Jul 6, 2026

Copy link
Copy Markdown

This PR implements seven ILP formulations of differing strengths to solve the graph edit distance problem, based on D'ascenzo, Andrea, et al. "Enhancing Graph Edit Distance Computation: Stronger and Orientation-based ILP Formulations." Proceedings of the VLDB Endowment 18.11 (2025): 4737-4749.

I have built the docs successfully locally, but I had some issues which are not resolved here because it seemed out of scope for this PR:

  • I have not used Documenter before and did not find a documentation on how exactly the make.jl script is supposed to be used. With the help of ChatGPT, I stumbled on the following working script, to be called from project root:
using Pkg

Pkg.activate("docs")
Pkg.develop(path=".")
Pkg.instantiate()

include("docs/make.jl")
  • The Project.toml and docs/Project.toml contradict each other on the required Documenter version. After deleting the line in Project.toml, the build works, but as I am inexperienced I don't know if this is the correct fix
  • the makedocs call in make.jl throws an error for each of the internal docstrings I wrote because they are not exposed in the documentation. The (again LLM-provided) fix is to add checkdocs=:exports as an argument. At least for the current documentation structure, I believe this is the correct fix. It makes little sense to delete internal documentation, but at the same time algorithms.md is definitely the wrong place to put all those and in my opinion they shouldn't be exposed to users anyway.

there are three kinds of variable structures. We break symmetry by
requiring edge indices be ordered from i to j if i < j.
These constraints encode graph structure
This is much cleaner and will prevent use of if statements later
The function checks if the graphs are undirected and defines a slightly
nicer call interface
I could not find a way to account for the more general type of the empty
variable sets if a graph has no edges, so now we just don't typehint the
edge variables.
If the edgeset of one graph is empty, the edge map variables are
nonexistent. In this case, the julia compiler doesn't know how it should
handle sum() over an empty set of variables. This commit adds a default
return value of 0, which fixes the crashes.
They were removed to accomodate for the edge case of empty graphs.
It returns the node matching matrix
These are the most important ones, because they are the public facing
entrypoints.
Internal, so only rough description.
Since the functions are internal, only rough documentation on what
implication is implemented are added. Technically inferrable from code,
but I think it provides value.
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.67742% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.71%. Comparing base (f2cf29d) to head (7604fca).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
src/graph_edit_distance.jl 84.67% 19 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (f2cf29d) and HEAD (7604fca). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (f2cf29d) HEAD (7604fca)
2 1
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #37      +/-   ##
==========================================
- Coverage   98.33%   92.71%   -5.63%     
==========================================
  Files           8       10       +2     
  Lines         180      343     +163     
==========================================
+ Hits          177      318     +141     
- Misses          3       25      +22     

☔ View full report in Codecov by Harness.
📢 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.

@jhandwe jhandwe marked this pull request as draft July 6, 2026 11:20
@aurorarossi

Copy link
Copy Markdown
Member

Thank you, Jonas, for the PR.

Regarding the documentation, I think you are right: it now errors because there is a docstring for a non-exported function.

For the downgrade test, the problem seems to be that you are using GenericModel instead of Model. Is there a specific reason for this, or can we simply use Model, as in the rest of the library?

For Julia 1.6, the issue can be reproduced with this MWE:

using JuMP

model = Model()
@variable(model, x[1:2])
@variable(model, y[1:2, 1:2])

# This works on Julia 1.6.7
println(sum(y[1, :]; init=0))

# This works inside JuMP
@constraint(model, [i in 1:2], sum(y[i, :]) <= x[i])

# This also works inside JuMP
@constraint(model, [i in 1:2], sum((y[i, j] for j in 1:2); init=0) <= x[i])

# This fails on Julia 1.6.7
# ERROR: syntax: malformed expression
@constraint(model, [i in 1:2], sum(y[i, :]; init=0) <= x[i])

Would it be possible to change the code to avoid this syntax, so that we can keep supporting the same Julia versions currently supported by the library?

@jhandwe

jhandwe commented Jul 6, 2026

Copy link
Copy Markdown
Author

I believe I used GenericModel because that is the type the JuMP docs themselves use for types in functions which apply to any model (e.g. here). But there is no specific reason beyond that why it has to be that type; reading the docs again I am not exactly sure of the difference between JuMP.Model and JuMP.GenericModel.

On the index syntax, I've just now tested a replacement, which works. The issue is that it makes the code much, much more verbose because the y[i, :] syntax can implicity handle sparse indexing. For example, see the following lines of my code:

# existing syntax
sum(vars.z[i, :, k, l]; init=0)

# version without `:` indexing
sum(vars.z[i, j, k, l] for j in 1:nv(G) if has_edge(G, i,j) && i < j; init=0)

It more than doubles the character count and makes it much harder to maintain as well, since we have to explicitly respect the ordering of edges.

Is there a reason why the tests run against Julia 1.6? As far as I can tell, that was the LTS version before it was switched to 1.10 already some time ago. The JuMP library itself dropped support for Julia 1.6 a few months ago, and the current version supports Julia 1.10. Based on this, I think it is reasonable to raise the support in this library to the current LTS version 1.10.11, which would allow us to keep the cleaner slicing syntax here.

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.

3 participants