-
Notifications
You must be signed in to change notification settings - Fork 268
Expand file tree
/
Copy pathlibraries.py
More file actions
317 lines (254 loc) · 11.3 KB
/
Copy pathlibraries.py
File metadata and controls
317 lines (254 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def _export_graph_to_networkx(
graph,
create_using=None,
vertex_attr_hashable: str = "_nx_name",
):
"""Converts the graph to networkx format.
igraph has ordered vertices and edges, but networkx does not. To keep
track of the original order, the '_igraph_index' vertex property is
added to both vertices and edges.
@param create_using: specifies which NetworkX graph class to use when
constructing the graph. C{None} means to let igraph infer the most
appropriate class based on whether the graph is directed and whether
it has multi-edges.
@param vertex_attr_hashable: vertex attribute used to name vertices
in the exported network. The default "_nx_name" ensures round trip
conversions to/from networkx are lossless.
"""
import networkx as nx
# Graph: decide on directness and mutliplicity
if create_using is None:
if graph.has_multiple():
cls = nx.MultiDiGraph if graph.is_directed() else nx.MultiGraph
else:
cls = nx.DiGraph if graph.is_directed() else nx.Graph
else:
cls = create_using
# Graph attributes
kw = {x: graph[x] for x in graph.attributes()}
g = cls(**kw)
multigraph = isinstance(g, (nx.MultiGraph, nx.MultiDiGraph))
# Nodes and node attributes
for i, v in enumerate(graph.vs):
vattr = v.attributes()
vattr["_igraph_index"] = i
# use _nx_name if the attribute is present so we can achieve
# a lossless round-trip in terms of vertex names
if vertex_attr_hashable in vattr:
hashable = vattr.pop(vertex_attr_hashable)
else:
hashable = i
# adding nodes one at a time is not slower in networkx
g.add_node(hashable, **vattr)
# Edges and edge attributes
for i, edge in enumerate(graph.es):
eattr = edge.attributes()
eattr["_igraph_index"] = i
if multigraph and "_nx_multiedge_key" in eattr:
eattr["key"] = eattr.pop("_nx_multiedge_key")
if vertex_attr_hashable in graph.vertex_attributes():
hashable_source = graph.vs[vertex_attr_hashable][edge.source]
hashable_target = graph.vs[vertex_attr_hashable][edge.target]
else:
hashable_source = edge.source
hashable_target = edge.target
# adding edges one at a time is not slower in networkx
g.add_edge(hashable_source, hashable_target, **eattr)
return g
def _construct_graph_from_networkx(cls, g, vertex_attr_hashable: str = "_nx_name"):
"""Converts the graph from networkx
Vertex names will be stored as a vertex_attr_hashable attribute (usually
"_nx_name", but see below). Because igraph stored vertices in an
ordered manner, vertices will get new IDs from 0 up. In case of
multigraphs, each edge will have an "_nx_multiedge_key" attribute, to
distinguish edges that connect the same two vertices.
@param g: networkx Graph or DiGraph
@param vertex_attr_hashable: attribute used to store the Python
hashable used by networkx to identify each vertex. The default value
'_nx_name' ensures lossless round trip conversions to/from networkx. An
alternative choice is 'name': in that case, using strings for vertex
names is recommended and, if the graph is re-exported to networkx,
Graph.to_networkx(vertex_attr_hashable="name") must be used to recover
the correct vertex nomenclature in the exported network.
"""
import networkx as nx
# Graph attributes
gattr = dict(g.graph)
# Nodes
vnames = list(g.nodes)
vattr = {vertex_attr_hashable: vnames}
vcount = len(vnames)
# Dictionary connecting networkx hashables with igraph indices
if len(g) and "_igraph_index" in next(iter(g.nodes.values())):
# Collect _igraph_index and fill gaps
idx = [x["_igraph_index"] for v, x in g.nodes.data()]
idx.sort()
idx_dict = {x: i for i, x in enumerate(idx)}
vd = {}
for v, datum in g.nodes.data():
vd[v] = idx_dict[datum["_igraph_index"]]
else:
vd = {v: i for i, v in enumerate(vnames)}
# NOTE: we do not need a special class for multigraphs, it is taken
# care for at the edge level rather than at the graph level.
graph = cls(
n=vcount, directed=g.is_directed(), graph_attrs=gattr, vertex_attrs=vattr
)
# Vertex attributes
for v, datum in g.nodes.data():
for key, val in datum.items():
# Get rid of _igraph_index (we used it already)
if key == "_igraph_index":
continue
graph.vs[vd[v]][key] = val
# Edges and edge attributes
eattr_names = {name for (_, _, data) in g.edges.data() for name in data}
eattr = {name: [] for name in eattr_names}
edges = []
# Multigraphs need a hidden attribute for multiedges
if isinstance(g, (nx.MultiGraph, nx.MultiDiGraph)):
eattr["_nx_multiedge_key"] = []
for u, v, edgekey, data in g.edges.data(keys=True):
edges.append((vd[u], vd[v]))
for name in eattr_names:
eattr[name].append(data.get(name))
eattr["_nx_multiedge_key"].append(edgekey)
else:
for u, v, data in g.edges.data():
edges.append((vd[u], vd[v]))
for name in eattr_names:
eattr[name].append(data.get(name))
# Sort edges if there is a trace of a previous igraph ordering
if "_igraph_index" in eattr:
# Poor man's argsort
sortd = list(enumerate(eattr["_igraph_index"]))
sortd.sort(key=lambda x: x[1])
idx = [i for i, x in sortd]
# Get rid of the _igraph_index now
del eattr["_igraph_index"]
# Sort edges
edges = [edges[i] for i in idx]
# Sort each attribute
eattr = {key: [val[i] for i in idx] for key, val in eattr.items()}
graph.add_edges(edges, eattr)
return graph
def _export_graph_to_graph_tool(
graph, graph_attributes=None, vertex_attributes=None, edge_attributes=None
):
"""Converts the graph to graph-tool
Data types: graph-tool only accepts specific data types. See the
following web page for a list:
https://graph-tool.skewed.de/static/doc/quickstart.html
Note: because of the restricted data types in graph-tool, vertex and
edge attributes require to be type-consistent across all vertices or
edges. If you set the property for only some vertices/edges, the other
will be tagged as None in igraph, so they can only be converted
to graph-tool with the type 'object' and any other conversion will
fail.
@param graph_attributes: dictionary of graph attributes to transfer.
Keys are attributes from the graph, values are data types (see
below). C{None} means no graph attributes are transferred.
@param vertex_attributes: dictionary of vertex attributes to transfer.
Keys are attributes from the vertices, values are data types (see
below). C{None} means no vertex attributes are transferred.
@param edge_attributes: dictionary of edge attributes to transfer.
Keys are attributes from the edges, values are data types (see
below). C{None} means no vertex attributes are transferred.
"""
import graph_tool as gt
# Graph
g = gt.Graph(directed=graph.is_directed())
# Nodes
vc = graph.vcount()
g.add_vertex(vc)
# Graph attributes
if graph_attributes is not None:
for x, dtype in graph_attributes.items():
# Strange syntax for setting internal properties
gprop = g.new_graph_property(str(dtype))
g.graph_properties[x] = gprop
g.graph_properties[x] = graph[x]
# Vertex attributes
if vertex_attributes is not None:
for x, dtype in vertex_attributes.items():
# Create a new vertex property
g.vertex_properties[x] = g.new_vertex_property(str(dtype))
# Fill the values from the igraph.Graph
for i in range(vc):
g.vertex_properties[x][g.vertex(i)] = graph.vs[i][x]
# Edges and edge attributes
if edge_attributes is not None:
for x, dtype in edge_attributes.items():
g.edge_properties[x] = g.new_edge_property(str(dtype))
for edge in graph.es:
e = g.add_edge(edge.source, edge.target)
if edge_attributes is not None:
for x in edge_attributes.keys():
prop = edge.attributes().get(x, None)
g.edge_properties[x][e] = prop
return g
def _construct_graph_from_graph_tool(cls, g):
"""Converts the graph from graph-tool
@param g: graph-tool Graph
"""
# Graph attributes
gattr = dict(g.graph_properties)
# Nodes
vcount = g.num_vertices()
# Graph
graph = cls(n=vcount, directed=g.is_directed(), graph_attrs=gattr)
# Node attributes
for key, val in g.vertex_properties.items():
# val.get_array() returns None for non-scalar types so use the slower
# way if this happens
prop = val.get_array()
arr = prop if prop is not None else val
for i in range(vcount):
graph.vs[i][key] = arr[i]
# Edges and edge attributes
# NOTE: graph-tool is quite strongly typed, so each property is always
# defined for all edges, using default values for the type. E.g. for a
# string property/attribute the missing edges get an empty string.
edges = []
eattr_names = list(g.edge_properties)
eattr = {name: [] for name in eattr_names}
for e in g.edges():
edges.append((int(e.source()), int(e.target())))
for name, attr_map in g.edge_properties.items():
eattr[name].append(attr_map[e])
graph.add_edges(edges, eattr)
return graph
def _export_graph_to_torch_geometric(
graph, vertex_attributes=None, edge_attributes=None
):
"""Converts the graph to torch geometric
Data types: graph-tool only accepts specific data types. See the
following web page for a list:
https://pytorch-geometric.readthedocs.io/en/latest/generated/torch_geometric.data.Data.html#torch_geometric.data.Data
@param g: graph-tool Graph
@param vertex_attributes: dictionary of vertex attributes to transfer.
Keys are attributes from the vertices, values are data types (see
below). C{None} means no vertex attributes are transferred.
@param edge_attributes: dictionary of edge attributes to transfer.
Keys are attributes from the edges, values are data types (see
below). C{None} means no vertex attributes are transferred.
"""
import torch
from torch_geometric.data import Data
if vertex_attributes is None:
vertex_attributes = graph.vertex_attributes()
if edge_attributes is None:
edge_attributes = graph.edge_attributes()
# Edge index
edge_index = torch.tensor(graph.get_edgelist(), dtype=torch.long)
# Node attributes
x = torch.tensor([graph.vs[attr] for attr in vertex_attributes])
if x.ndim > 1:
x = x.permute(*torch.arange(x.ndim - 1, -1, -1))
# Edge attributes
edge_attr = torch.tensor([graph.es[attr] for attr in edge_attributes])
if edge_attr.ndim > 1:
edge_attr = edge_attr.permute(*torch.arange(edge_attr.ndim - 1, -1, -1))
# Wrap into correct data structure
data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
return data