From f69ed4c2c795300aff03cd1d101318f4e725d681 Mon Sep 17 00:00:00 2001 From: fchrstou Date: Mon, 26 Jun 2023 11:18:55 +0200 Subject: [PATCH] Support MetaGraphs.jl --- Project.toml | 6 +- README.md | 24 +- ext/GraphIOMetaGraphsGraphMLExt.jl | 363 +++++++++++++++++++++++++++++ src/GraphIO.jl | 3 + test/GraphML/runtests.jl | 57 +++++ test/testdata/mlattrs.graphml | 142 +++++++++++ 6 files changed, 583 insertions(+), 12 deletions(-) create mode 100644 ext/GraphIOMetaGraphsGraphMLExt.jl create mode 100644 test/testdata/mlattrs.graphml diff --git a/Project.toml b/Project.toml index 918fc3d..2d9d6e9 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" [weakdeps] CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" +MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5" ParserCombinator = "fae87a5f-d1ad-5cf0-8f61-c941e1580b46" [extensions] @@ -19,12 +20,14 @@ GraphIOGEXFExt = "EzXML" GraphIOGMLExt = "ParserCombinator" GraphIOGraphMLExt = "EzXML" GraphIOLGCompressedExt = "CodecZlib" +GraphIOMetaGraphsGraphMLExt = ["EzXML", "MetaGraphs"] [compat] CodecZlib = "0.7" DelimitedFiles = "1" EzXML = "1" Graphs = "1.4" +MetaGraphs = "0.7.2" ParserCombinator = "2.1" Requires = "1" SimpleTraits = "0.9" @@ -36,8 +39,9 @@ CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5" ParserCombinator = "fae87a5f-d1ad-5cf0-8f61-c941e1580b46" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "CodecZlib", "Graphs", "JuliaFormatter", "EzXML", "ParserCombinator", "Test"] +test = ["Aqua", "CodecZlib", "Graphs", "JuliaFormatter", "EzXML", "ParserCombinator", "Test", "MetaGraphs"] diff --git a/README.md b/README.md index 2c745e2..7e2f614 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,19 @@ GraphIO provides support to [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl Currently, the following functionality is provided: -| Format | Read | Write | Multiple Graphs | Format Name | Comment | -| ----------- | ---- | ----- | --------------- | -------------- | ------------------------------------------------------------------------------------------- | -| EdgeList | ✓ | ✓ | | EdgeListFormat | a simple list of sources and dests separated by whitespace and/or comma, one pair per line. | -| [GML] | ✓ | ✓ | ✓ | GMLFormat | | -| [Graph6] | ✓ | ✓ | ✓ | Graph6Format | | -| [GraphML] | ✓ | ✓ | ✓ | GraphMLFormat | | -| [Pajek NET] | ✓ | ✓ | | NETFormat | | -| [GEXF] | | ✓ | | GEXFFormat | | -| [DOT] | ✓ | | ✓ | DOTFormat | | -| [CDF] | ✓ | | | CDFFormat | | - +| Format | Read | Write | Multiple Graphs | Format Name | Simple(Di)Graph | MetaGraphs.jl | +| ----------- | ---- | ----- | --------------- | -------------- | --------------- | ------------- | +| EdgeList [^1] | ✓ | ✓ | | EdgeListFormat | ✓ | | +| [GML] | ✓ | ✓ | ✓ | GMLFormat | ✓ | | +| [Graph6] | ✓ | ✓ | ✓ | Graph6Format | ✓ | | +| [GraphML] | ✓ | ✓ | ✓ | GraphMLFormat | ✓ | ✓ | +| [Pajek NET] | ✓ | ✓ | | NETFormat | ✓ | | +| [GEXF] | | ✓ | | GEXFFormat | ✓ | | +| [DOT] | ✓ | | ✓ | DOTFormat | ✓ | | +| [CDF] | ✓ | | | CDFFormat | ✓ | | + + +[^1]: a simple list of sources and dests separated by whitespace and/or comma, one pair per line. Graphs are read using either the `loadgraph` function or, for formats that support multiple graphs in a single file, the `loadgraphs` functions. `loadgraph` returns a Graph object, while `loadgraphs` returns a dictionary of Graph objects. diff --git a/ext/GraphIOMetaGraphsGraphMLExt.jl b/ext/GraphIOMetaGraphsGraphMLExt.jl new file mode 100644 index 0000000..2102bec --- /dev/null +++ b/ext/GraphIOMetaGraphsGraphMLExt.jl @@ -0,0 +1,363 @@ +module GraphIOMetaGraphsGraphMLExt + +using Graphs +import Graphs: loadgraph, loadgraphs, savegraph + +@static if isdefined(Base, :get_extension) + using GraphIO + using EzXML, MetaGraphs + import GraphIO.GraphML.GraphMLFormat + import MetaGraphs: AbstractMetaGraph, MGFormat +else # not required for julia >= v1.9 + using ..GraphIO + using ..EzXML, ..MetaGraphs + import ..GraphIO.GraphML.GraphMLFormat + import ..MetaGraphs: AbstractMetaGraph, MGFormat +end + +@enum GraphlMLAttributesDomain atgraph atnode atedge atall +const graphlMLAttributesDomain = Dict( + "graph" => atgraph, "node" => atnode, "edge" => atedge, "all" => atall +) + +@enum GraphlMLAttributesType atboolean atint atlong atfloat atdouble atstring +const graphMLAttributesType = Dict( + "int" => Int, + "boolean" => Bool, + "long" => Int128, + "float" => Float64, + "double" => Float64, + "string" => String, +) + +const graphMLAttributesType_rev = Dict( + Bool => "boolean", Integer => "long", Real => "float", AbstractString => "string" +) + +struct AttrKey{T} + id::String + name::String + domain::GraphlMLAttributesDomain + type::Type{T} + default::Union{T,Nothing} +end + +# +## probably better to put in another file +# + +getvectortype(::Vector{T}) where {T} = T + +function getnodekeys(dmg::Dict) + return _getelementkeys([ + Pair(k, v) for vpg in getfield.(values(dmg), :vprops) for (k, v) in vpg + ]) +end +getnodekeys(mg::AbstractMetaGraph) = _getelementkeys([Pair(k, v) for (k, v) in mg.vprops]) + +function getedgekeys(dmg::Dict) + return _getelementkeys([ + Pair(k, v) for vpg in getfield.(values(dmg), :eprops) for (k, v) in vpg + ]) +end +getedgekeys(mg::AbstractMetaGraph) = _getelementkeys([Pair(k, v) for (k, v) in mg.eprops]) + +function _getelementkeys(dprops) + pairs = [Pair(x, y) for d in getfield.(dprops, :second) for (x, y) in d] + nodefieldset = Set(getfield.(pairs, :first)) + nodefieldsettypes = [ + getvectortype([p.second for p in pairs if p.first == nfs]) for nfs in nodefieldset + ] + return nodefieldset, nodefieldsettypes +end + +function getcompatiblesupertype(elementtype) + if elementtype <: Bool + return Bool + elseif elementtype <: Integer + return Integer + elseif elementtype <: Real + return Real + else + return AbstractString + end +end + +function savemetagraphkeys(mg::Dict{String,T}, xroot) where {T<:AbstractMetaGraph} + for (ndf, nt) in zip(getnodekeys(mg)...) + xk = addelement!(xroot, "key") + xk["attr.name"] = string(ndf) + xk["attr.type"] = graphMLAttributesType_rev[getcompatiblesupertype(nt)] + xk["for"] = "node" + xk["id"] = string(ndf) + end + for (ndf, nt) in zip(getedgekeys(mg)...) + xk = addelement!(xroot, "key") + xk["attr.name"] = string(ndf) + xk["attr.type"] = graphMLAttributesType_rev[getcompatiblesupertype(nt)] + xk["for"] = "node" + xk["id"] = string(ndf) + end +end + +function startgraphmlroot(xdoc) + xroot = setroot!(xdoc, ElementNode("graphml")) + xroot["xmlns"] = "http://graphml.graphdrawing.org/xmlns" + xroot["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance" + xroot["xsi:schemaLocation"] = "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd" + return xroot +end + +function addallvertswithid(ig, xg) + for v in vertices(ig) + xv = addelement!(xg, "node") + xv["id"] = get_prop(ig, v, :id) + for (k, v) in props(ig, v) + k == :id && continue + xel = addelement!(xv, "data", string(v)) + xel["key"] = k + end + end +end +function addalledgeswithid(ig, xg) + for e in edges(ig) + xe = addelement!(xg, "edge") + xe["id"] = get_prop(ig, e, :id) + xe["source"] = get_prop(ig, src(e), :id) + xe["target"] = get_prop(ig, dst(e), :id) + for (k, v) in props(ig, e) + k == :id && continue + xel = addelement!(xe, "data", string(v)) + xel["key"] = k + end + end +end +# +## another file +# + +function instantiatemetagraph(graphnode::EzXML.Node) + return graphnode["edgedefault"] == "directed" ? MetaDiGraph() : MetaGraph() +end +function metagraphtype(graphnode::EzXML.Node) + return if graphnode["edgedefault"] == "directed" + MetaDiGraph{Int,Float64} + else + MetaGraph{Int,Float64} + end +end + +function nodedefaults(keyprops::Dict{String,AttrKey}) + return collect( + Iterators.filter( + v -> getfield(v, :default) !== nothing && getfield(v, :domain) == atnode, + values(keyprops), + ), + ) +end +function edgedefaults(keyprops::Dict{String,AttrKey}) + return collect( + Iterators.filter( + v -> getfield(v, :default) !== nothing && getfield(v, :domain) == atedge, + values(keyprops), + ), + ) +end + +function addmetagraphmlnode!( + gr::AbstractGraph, + node::EzXML.Node, + defaults::Vector{AttrKey}, + keyprops::Dict{String,AttrKey}, + ns::String, +) + add_vertex!(gr) + i = length(vertices(gr)) + set_prop!(gr, i, :id, node["id"]) + for def in defaults + set_prop!(gr, i, Symbol(def.name), def.default) + end + for data in findall("x:data", node, ["x" => ns]) + set_prop!( + gr, + i, + Symbol(keyprops[data["key"]].name), + if keyprops[data["key"]].type == String + strip(nodecontent(data)) + else + parse(keyprops[data["key"]].type, nodecontent(data)) + end, + ) + end +end + +function addmetagraphmledge!( + gr::AbstractGraph, + edge::EzXML.Node, + defaults::Vector{AttrKey}, + keyprops::Dict{String,AttrKey}, + ns::String, +) + srcnode = gr[edge["source"], :id] + trgnode = gr[edge["target"], :id] + add_edge!(gr, srcnode, trgnode) + set_prop!(gr, srcnode, trgnode, :id, edge["id"]) + for def in defaults + set_prop!(gr, srcnode, trgnode, Symbol(def.name), def.default) + end + for data in findall("x:data", edge, ["x" => ns]) + set_prop!( + gr, + srcnode, + trgnode, + Symbol(keyprops[data["key"]].name), + if keyprops[data["key"]].type == String + strip(nodecontent(data)) + else + parse(keyprops[data["key"]].type, nodecontent(data)) + end, + ) + end +end + +function _get_key_props(doc::EzXML.Document) + ns = namespace(doc.root) + keynodes = findall("//x:key", doc.root, ["x" => ns]) + keyprops = Dict{String,AttrKey}() + for keynode in keynodes + attrtype = graphMLAttributesType[strip(keynode["attr.type"])] + keyadded = false + for childnode in EzXML.eachnode(keynode) + if EzXML.nodename(childnode) == "default" + defaultcontent = strip(nodecontent(childnode)) + keyprops[keynode["id"]] = AttrKey( + keynode["id"], + keynode["attr.name"], + graphlMLAttributesDomain[keynode["for"]], + attrtype, + attrtype == String ? defaultcontent : parse(attrtype, defaultcontent), + ) + keyadded = true + end + end + if !keyadded + keyprops[keynode["id"]] = AttrKey( + keynode["id"], + keynode["attr.name"], + graphlMLAttributesDomain[keynode["for"]], + attrtype, + nothing, + ) + end + end + return keyprops +end + +function _loadmetagraph_fromnode(graphnode::EzXML.Node, keyprops::Dict{String,AttrKey}) + ns = namespace(graphnode) + gr = instantiatemetagraph(graphnode) + set_prop!(gr, :id, graphnode["id"]) + set_indexing_prop!(gr, :id) + for (i, node) in enumerate(findall("x:node", graphnode, ["x" => ns])) + addmetagraphmlnode!(gr, node, nodedefaults(keyprops), keyprops, ns) + end + + for edge in findall("x:edge", graphnode, ["x" => ns]) + addmetagraphmledge!(gr, edge, edgedefaults(keyprops), keyprops, ns) + end + return gr +end + +#TODO carefull if graphml format is nested +function loadmetagraphml(io::IO, gname::String) + doc = readxml(io) + ns = namespace(doc.root) + keyprops = _get_key_props(doc) + + for graphnode in findall("//x:graph", doc.root, ["x" => ns]) + if graphnode["id"] == gname + return _loadmetagraph_fromnode(graphnode, keyprops) + end + end +end +function loadmetagraphml_mult(io::IO) + doc = readxml(io) + ns = namespace(doc.root) + keyprops = _get_key_props(doc) + + graphnodes = findall("//x:graph", doc.root, ["x" => ns]) + + graphs = Dict{String,AbstractMetaGraph}() + for graphnode in graphnodes + graphs[graphnode["id"]] = _loadmetagraph_fromnode(graphnode, keyprops) + end + return graphs +end + +function savemetagraphml_mult(io::IO, dmg::Dict{String,T}) where {T<:AbstractMetaGraph} + xdoc = XMLDocument() + xroot = startgraphmlroot(xdoc) + savemetagraphkeys(dmg, xroot) + + # add graph + for (gname, mg) in dmg + xg = addelement!(xroot, "graph") + xg["id"] = gname + xg["edgedefault"] = is_directed(mg) ? "directed" : "undirected" + + for i in 1:nv(mg) + xv = addelement!(xg, "node") + if has_prop(mg, i, :id) + xv["id"] = get_prop(mg, i, :id) + else + xv["id"] = "n$(i-1)" + end + for (k, v) in props(mg, i) + k == :id && continue + xel = addelement!(xv, "data", string(v)) + xel["key"] = k + end + end + + m = 0 + for e in Graphs.edges(mg) + xe = addelement!(xg, "edge") + + if has_prop(mg, e, :id) + xe["id"] = get_prop(mg, e, :id) + else + xe["id"] = "e$(m)" + end + + if has_prop(mg, src(e), :id) + xe["source"] = get_prop(mg, src(e), :id) + else + xe["source"] = "n$(src(e)-1)" + end + if has_prop(mg, dst(e), :id) + xe["target"] = get_prop(mg, dst(e), :id) + else + xe["target"] = "n$(dst(e)-1)" + end + + for (k, v) in props(mg, e) + k == :id && continue + xel = addelement!(xe, "data", string(v)) + xel["key"] = k + end + m += 1 + end + end + + prettyprint(io, xdoc) + return nothing +end + +loadgraph(io::IO, gname::String, ::GraphMLFormat, ::MGFormat) = loadmetagraphml(io, gname) +loadgraphs(io::IO, ::GraphMLFormat, ::MGFormat) = loadmetagraphml_mult(io) +function savegraph(io::IO, g::AbstractMetaGraph, gname::String, ::GraphMLFormat) + return savemetagraphml_mult(io, Dict(gname => g)) +end +savegraph(io::IO, dg::Dict, ::GraphMLFormat, ::MGFormat) = savemetagraphml_mult(io, dg) + +end diff --git a/src/GraphIO.jl b/src/GraphIO.jl index b429bb6..518776a 100644 --- a/src/GraphIO.jl +++ b/src/GraphIO.jl @@ -12,6 +12,9 @@ end @require EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" begin include("../ext/GraphIOGEXFExt.jl") include("../ext/GraphIOGraphMLExt.jl") + @require MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5" begin + include("../ext/GraphIOMetaGraphsGraphMLExt.jl") + end end @require ParserCombinator = "fae87a5f-d1ad-5cf0-8f61-c941e1580b46" begin include("../ext/GraphIODOTExt.jl") diff --git a/test/GraphML/runtests.jl b/test/GraphML/runtests.jl index ce5d672..0761da2 100644 --- a/test/GraphML/runtests.jl +++ b/test/GraphML/runtests.jl @@ -1,6 +1,7 @@ using Test using EzXML using GraphIO.GraphML +using MetaGraphs @testset "GraphML" begin for g in values(allgraphs) @@ -18,3 +19,59 @@ using GraphIO.GraphML d = loadgraphs(fname, GraphMLFormat()) write_test(GraphMLFormat(), d) end + +function test_read_metagraph(dmg) + for v in vertices(dmg) + if get_prop(dmg, v, :id) == "N6" + @test get_prop(dmg, v, :VertexLabel) == "N6" + @test get_prop(dmg, v, :xcoord) == 170 + @test get_prop(dmg, v, :ycoord) == 0 + end + end + for e in edges(dmg) + if get_prop(dmg, e, :id) == "N0-N3" + @test get_prop(dmg, e, :LinkCapacity) == 100 + end + end +end + +@testset "MetaGraphsGraphML" begin + # single graph + fname = joinpath(testdir, "testdata", "mlattrs.graphml") + mg = open(fname, "r") do io + loadgraph(io, "main-graph", GraphMLFormat(), MGFormat()) + end + test_read_metagraph(mg) + + # re-read must be equal + ftname = joinpath(testdir, "testdata", "mlattrs_main-graph_write.graphml") + savegraph(ftname, mg, "main-graph", GraphMLFormat()) + mg2 = open(ftname, "r") do io + loadgraph(io, "main-graph", GraphMLFormat(), MGFormat()) + end + @test mg == mg2 && mg.vprops == mg2.vprops && mg.eprops == mg2.eprops + rm(ftname) + + # multiple graphs + dmg = open(fname, "r") do io + loadgraphs(io, GraphMLFormat(), MGFormat()) + end + + @test length(dmg) == 2 + test_read_metagraph(dmg["main-graph"]) + + # re-read must be equal + ftname = joinpath(testdir, "testdata", "mlattrs_write.graphml") + open(ftname, "w") do io + savegraph(io, dmg, GraphMLFormat(), MGFormat()) + end + dmg2 = open(ftname, "r") do io + loadgraphs(io, GraphMLFormat(), MGFormat()) + end + for (dmg_g, dmg2_g) in zip(values(dmg), values(dmg2)) + @test dmg_g == dmg2_g && + dmg_g.vprops == dmg2_g.vprops && + dmg_g.eprops == dmg2_g.eprops + end + rm(ftname) +end diff --git a/test/testdata/mlattrs.graphml b/test/testdata/mlattrs.graphml new file mode 100644 index 0000000..753d6da --- /dev/null +++ b/test/testdata/mlattrs.graphml @@ -0,0 +1,142 @@ + + + + + + + + 100.0 + + + + N0 + 0.0 + 0.0 + + + N1 + 10.0 + 100.0 + + + N2 + 110.0 + 100.0 + + + N3 + 100.0 + 0.0 + + + N4 + 20.0 + 200.0 + + + N5 + 120.0 + 200.0 + + + N6 + 170.0 + 0.0 + + + N7 + 230.0 + -100.0 + + + N8 + 260.0 + 140.0 + + + N9 + 300.0 + 0.0 + + + N10 + 310.0 + 200.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + N0 + 0.0 + 0.0 + + + N1 + 10.0 + 100.0 + + + N2 + 110.0 + 100.0 + + + N3 + 100.0 + 0.0 + + + + + +