Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ jobs:
fail-fast: false
matrix:
version:
- '1'
- '1.6'
- '1.9'
- 'nightly'
os:
- ubuntu-latest
arch:
Expand Down
17 changes: 15 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "GraphIO"
uuid = "aa1b3936-2fda-51b9-ab35-c553d3a640a2"
version = "0.6.0"
version = "0.7.0"

[deps]
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
Expand All @@ -9,13 +9,21 @@ Requires = "ae029012-a4dd-5104-9daa-d747884805df"
SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d"

[compat]
CodecZlib = "0.7"
EzXML = "1"
Graphs = "1.4"
ParserCombinator = "2.1"
Graphs = "1.4"
Requires = "1"
SimpleTraits = "0.9"
julia = "1"

[extensions]
GraphIODOTExt = "ParserCombinator"
GraphIOGEXFExt = "EzXML"
GraphIOGMLExt = "ParserCombinator"
GraphIOGraphMLExt = "EzXML"
GraphIOLGCompressedExt = "CodecZlib"

[extras]
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
Expand All @@ -25,3 +33,8 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["CodecZlib", "Graphs", "EzXML", "ParserCombinator", "Test"]

[weakdeps]
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
ParserCombinator = "fae87a5f-d1ad-5cf0-8f61-c941e1580b46"
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ Format | Read | Write | Multiple Graphs| Format Name |
[DOT] | ✓ | | ✓ |DOTFormat |
[CDF] | ✓ | | |CDFFormat |

[EdgeList]: a simple list of sources and dests separated by whitespace and/or comma, one pair per line.
[GML]: https://en.wikipedia.org/wiki/Graph_Modelling_Language
[Graph6]: https://users.cecs.anu.edu.au/~bdm/data/formats.html
[GraphML]: https://en.wikipedia.org/wiki/GraphML
[Pajek NET]: https://gephi.org/users/supported-graph-formats/pajek-net-format/
[GEXF]: https://gephi.org/gexf/format/
[DOT]: https://en.wikipedia.org/wiki/DOT_(graph_description_language)
[EdgeList]: a simple list of sources and dests separated by whitespace and/or comma, one pair per line. \
[GML]: https://en.wikipedia.org/wiki/Graph_Modelling_Language \
[Graph6]: https://users.cecs.anu.edu.au/~bdm/data/formats.html \
[GraphML]: https://en.wikipedia.org/wiki/GraphML \
[Pajek NET]: https://gephi.org/users/supported-graph-formats/pajek-net-format/ \
[GEXF]: https://gephi.org/gexf/format/ \
[DOT]: https://en.wikipedia.org/wiki/DOT_(graph_description_language) \

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.
Expand All @@ -35,4 +35,13 @@ For example, an edgelist file could be loaded as:
graph = loadgraph("path_to_graph/my_edgelist.txt", "graph_key", EdgeListFormat())
```

## Reading different graph types
All `*Format` types are readily available. However, in order to use some of them to `loadgraph` further dependencies are needed, which you need to load with `using` in advance.
- Reading [DOT] or [GML] files: do `using ParserCombinator`
- Reading [GEXF] or [GraphML] files: do `using EzXML`
- Reading [GML] files: do `using CodecZlib`

The current design avoids populating your environment with unnecessary dependencies.

> **_IMPLEMENTATION NOTE:_** The current design uses package extensions introduced in Julia v1.9. At the moment, package extensions cannot conditionally load types; that's one of the main reasons why all `*Format` types are readily accesible. However, the functionality of `loadgraph` is extended for the various types only when the appropriate dependencies are available. We are searching for more intuitive ways to interface this functionality.

92 changes: 92 additions & 0 deletions ext/GraphIODOTExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
module GraphIODOTExt

using Graphs
import Graphs: loadgraph, loadgraphs, savegraph

@static if isdefined(Base, :get_extension)
using GraphIO
using ParserCombinator
import GraphIO.DOT.DOTFormat
else # not required for julia >= v1.9
using ..GraphIO
using ..ParserCombinator
import ..GraphIO.DOT.DOTFormat
end


function savedot(io::IO, g::AbstractGraph, gname::String = "")
isdir = is_directed(g)
println(io,(isdir ? "digraph " : "graph ") * gname * " {")
for i in vertices(g)
println(io,"\t" * string(i))
end
if isdir
for u in vertices(g)
out_nbrs = outneighbors(g, u)
length(out_nbrs) == 0 && continue
println(io, "\t" * string(u) * " -> {" * join(out_nbrs,',') * "}")
end
else
for e in edges(g)
source = string(src(e))
dest = string(dst(e))
println(io, "\t" * source * " -- " * dest)
end
end
println(io,"}")
return 1
end

function savedot_mult(io::IO, graphs::Dict)
ng = 0
for (gname, g) in graphs
ng += savedot(io, g, gname)
end
return ng
end

function _dot_read_one_graph(pg::Parsers.DOT.Graph)
isdir = pg.directed
nvg = length(Parsers.DOT.nodes(pg))
nodedict = Dict(zip(collect(Parsers.DOT.nodes(pg)), 1:nvg))
if isdir
g = DiGraph(nvg)
else
g = Graph(nvg)
end
for es in Parsers.DOT.edges(pg)
s = nodedict[es[1]]
d = nodedict[es[2]]
add_edge!(g, s, d)
end
return g
end

_name(pg::Parsers.DOT.Graph) =
pg.id !== nothing ? pg.id.id :
Parsers.DOT.StringID(pg.directed ? "digraph" : "graph")

function loaddot(io::IO, gname::String)
p = Parsers.DOT.parse_dot(read(io, String))
for pg in p
_name(pg) == gname && return _dot_read_one_graph(pg)
end
error("Graph $gname not found")
end

function loaddot_mult(io::IO)
p = Parsers.DOT.parse_dot(read(io, String))
graphs = Dict{String,AbstractGraph}()

for pg in p
graphs[_name(pg)] = _dot_read_one_graph(pg)
end
return graphs
end

loadgraph(io::IO, gname::String, ::DOTFormat) = loaddot(io, gname)
loadgraphs(io::IO, ::DOTFormat) = loaddot_mult(io)
savegraph(io::IO, g::AbstractGraph, gname::String, ::DOTFormat) = savedot(io, g, gname)
savegraph(io::IO, d::Dict, ::DOTFormat) = savedot_mult(io, d)

end
59 changes: 59 additions & 0 deletions ext/GraphIOGEXFExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module GraphIOGEXFExt

using Graphs
import Graphs: loadgraph, loadgraphs, savegraph, AbstractGraph

@static if isdefined(Base, :get_extension)
using GraphIO
using EzXML
import GraphIO.GEXF.GEXFFormat
else # not required for julia >= v1.9
using ..GraphIO
using ..EzXML
import ..GraphIO.GEXF.GEXFFormat
end


"""
savegexf(f, g, gname)

Write a graph `g` with name `gname` to an IO stream `io` in the
[Gexf](http://gexf.net/format/) format. Return 1 (number of graphs written).
"""
function savegexf(io::IO, g::AbstractGraph, gname::String)
xdoc = XMLDocument()
xroot = setroot!(xdoc, ElementNode("gexf"))
xroot["xmlns"] = "http://www.gexf.net/1.2draft"
xroot["version"] = "1.2"
xroot["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
xroot["xsi:schemaLocation"] = "http://www.gexf.net/1.2draft/gexf.xsd"

xmeta = addelement!(xroot, "meta")
addelement!(xmeta, "description", gname)
xg = addelement!(xroot, "graph")
strdir = is_directed(g) ? "directed" : "undirected"
xg["defaultedgetype"] = strdir

xnodes = addelement!(xg, "nodes")
for i in 1:nv(g)
xv = addelement!(xnodes, "node")
xv["id"] = "$(i-1)"
end

xedges = addelement!(xg, "edges")
m = 0
for e in edges(g)
xe = addelement!(xedges, "edge")
xe["id"] = "$m"
xe["source"] = "$(src(e)-1)"
xe["target"] = "$(dst(e)-1)"
m += 1
end

prettyprint(io, xdoc)
return 1
end

savegraph(io::IO, g::AbstractGraph, gname::String, ::GEXFFormat) = savegexf(io, g, gname)

end
103 changes: 103 additions & 0 deletions ext/GraphIOGMLExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
module GraphIOGMLExt

using Graphs
import Graphs: loadgraph, loadgraphs, savegraph

@static if isdefined(Base, :get_extension)
using GraphIO
using ParserCombinator
import GraphIO.GML.GMLFormat
else # not required for julia >= v1.9
using ..GraphIO
using ..ParserCombinator
import ..GraphIO.GML.GMLFormat
end


function _gml_read_one_graph(gs, dir)
nodes = [x[:id] for x in gs[:node]]
if dir
g = DiGraph(length(nodes))
else
g = Graph(length(nodes))
end
mapping = Dict{Int,Int}()
for (i, n) in enumerate(nodes)
mapping[n] = i
end
sds = [(Int(x[:source]), Int(x[:target])) for x in gs[:edge]]
for (s, d) in (sds)
add_edge!(g, mapping[s], mapping[d])
end
return g
end

function loadgml(io::IO, gname::String)
p = Parsers.GML.parse_dict(read(io, String))
for gs in p[:graph]
dir = Bool(get(gs, :directed, 0))
graphname = get(gs, :label, dir ? "digraph" : "graph")

(gname == graphname) && return _gml_read_one_graph(gs, dir)
end
error("Graph $gname not found")
end

function loadgml_mult(io::IO)
p = Parsers.GML.parse_dict(read(io, String))
graphs = Dict{String,AbstractGraph}()
for gs in p[:graph]
dir = Bool(get(gs, :directed, 0))
graphname = get(gs, :label, dir ? "digraph" : "graph")
graphs[graphname] = _gml_read_one_graph(gs, dir)
end
return graphs
end

"""
savegml(f, g, gname="graph")

Write a graph `g` with name `gname` to an IO stream `io` in the
[GML](https://en.wikipedia.org/wiki/Graph_Modelling_Language) format. Return 1.
"""
function savegml(io::IO, g::AbstractGraph, gname::String = "")
println(io, "graph")
println(io, "[")
length(gname) > 0 && println(io, "label \"$gname\"")
is_directed(g) && println(io, "directed 1")
for i = 1:nv(g)
println(io, "\tnode")
println(io, "\t[")
println(io, "\t\tid $i")
println(io, "\t]")
end
for e in edges(g)
s, t = Tuple(e)
println(io, "\tedge")
println(io, "\t[")
println(io, "\t\tsource $s")
println(io, "\t\ttarget $t")
println(io, "\t]")
end
println(io, "]")
return 1
end


"""
savegml_mult(io, graphs)
Write a dictionary of (name=>graph) to an IO stream `io` Return number of graphs written.
"""
function savegml_mult(io::IO, graphs::Dict)
ng = 0
for (gname, g) in graphs
ng += savegml(io, g, gname)
end
return ng
end
loadgraph(io::IO, gname::String, ::GMLFormat) = loadgml(io, gname)
loadgraphs(io::IO, ::GMLFormat) = loadgml_mult(io)
savegraph(io::IO, g::AbstractGraph, gname::String, ::GMLFormat) = savegml(io, g, gname)
savegraph(io::IO, d::Dict, ::GMLFormat) = savegml_mult(io, d)

end
Loading