external_graph_libs.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. """Convert (to and) from rdflib graphs to other well known graph libraries.
  2. Currently the following libraries are supported:
  3. - networkx: MultiDiGraph, DiGraph, Graph
  4. - graph_tool: Graph
  5. Doctests in this file are all skipped, as we can't run them conditionally if
  6. networkx or graph_tool are available and they would err otherwise.
  7. see `../../test/test_extras_external_graph_libs.py` for conditional tests
  8. """
  9. from __future__ import annotations
  10. import logging
  11. from typing import TYPE_CHECKING, Any, Dict, List
  12. if TYPE_CHECKING:
  13. from rdflib.graph import Graph
  14. logger = logging.getLogger(__name__)
  15. def _identity(x):
  16. return x
  17. def _rdflib_to_networkx_graph(
  18. graph: Graph,
  19. nxgraph,
  20. calc_weights: bool,
  21. edge_attrs,
  22. transform_s=_identity,
  23. transform_o=_identity,
  24. ):
  25. """Helper method for multidigraph, digraph and graph.
  26. Modifies nxgraph in-place!
  27. Args:
  28. graph: an rdflib.Graph.
  29. nxgraph: a networkx.Graph/DiGraph/MultiDigraph.
  30. calc_weights: If True adds a 'weight' attribute to each edge according
  31. to the count of s,p,o triples between s and o, which is meaningful
  32. for Graph/DiGraph.
  33. edge_attrs: Callable to construct edge data from s, p, o.
  34. 'triples' attribute is handled specially to be merged.
  35. 'weight' should not be generated if calc_weights==True.
  36. (see invokers below!)
  37. transform_s: Callable to transform node generated from s.
  38. transform_o: Callable to transform node generated from o.
  39. """
  40. assert callable(edge_attrs)
  41. assert callable(transform_s)
  42. assert callable(transform_o)
  43. import networkx as nx
  44. for s, p, o in graph:
  45. ts, to = transform_s(s), transform_o(o) # apply possible transformations
  46. data = nxgraph.get_edge_data(ts, to)
  47. if data is None or isinstance(nxgraph, nx.MultiDiGraph):
  48. # no edge yet, set defaults
  49. data = edge_attrs(s, p, o)
  50. if calc_weights:
  51. data["weight"] = 1
  52. nxgraph.add_edge(ts, to, **data)
  53. else:
  54. # already have an edge, just update attributes
  55. if calc_weights:
  56. data["weight"] += 1
  57. if "triples" in data:
  58. d = edge_attrs(s, p, o)
  59. data["triples"].extend(d["triples"])
  60. def rdflib_to_networkx_multidigraph(
  61. graph: Graph, edge_attrs=lambda s, p, o: {"key": p}, **kwds
  62. ):
  63. r"""Converts the given graph into a networkx.MultiDiGraph.
  64. The subjects and objects are the later nodes of the MultiDiGraph.
  65. The predicates are used as edge keys (to identify multi-edges).
  66. Args:
  67. graph: a rdflib.Graph.
  68. edge_attrs: Callable to construct later edge_attributes. It receives
  69. 3 variables (s, p, o) and should construct a dictionary that is
  70. passed to networkx's add_edge(s, o, \*\*attrs) function.
  71. By default this will include setting the MultiDiGraph key=p here.
  72. If you don't want to be able to re-identify the edge later on, you
  73. can set this to `lambda s, p, o: {}`. In this case MultiDiGraph's
  74. default (increasing ints) will be used.
  75. Returns:
  76. networkx.MultiDiGraph
  77. Example:
  78. ```python
  79. >>> from rdflib import Graph, URIRef, Literal
  80. >>> g = Graph()
  81. >>> a, b, l = URIRef('a'), URIRef('b'), Literal('l')
  82. >>> p, q = URIRef('p'), URIRef('q')
  83. >>> edges = [(a, p, b), (a, q, b), (b, p, a), (b, p, l)]
  84. >>> for t in edges:
  85. ... g.add(t)
  86. ...
  87. >>> mdg = rdflib_to_networkx_multidigraph(g)
  88. >>> len(mdg.edges())
  89. 4
  90. >>> mdg.has_edge(a, b)
  91. True
  92. >>> mdg.has_edge(a, b, key=p)
  93. True
  94. >>> mdg.has_edge(a, b, key=q)
  95. True
  96. >>> mdg = rdflib_to_networkx_multidigraph(g, edge_attrs=lambda s,p,o: {})
  97. >>> mdg.has_edge(a, b, key=0)
  98. True
  99. >>> mdg.has_edge(a, b, key=1)
  100. True
  101. ```
  102. """
  103. import networkx as nx
  104. mdg = nx.MultiDiGraph()
  105. _rdflib_to_networkx_graph(graph, mdg, False, edge_attrs, **kwds)
  106. return mdg
  107. def rdflib_to_networkx_digraph(
  108. graph: Graph,
  109. calc_weights: bool = True,
  110. edge_attrs=lambda s, p, o: {"triples": [(s, p, o)]},
  111. **kwds,
  112. ):
  113. r"""Converts the given graph into a networkx.DiGraph.
  114. As an rdflib.Graph() can contain multiple edges between nodes, by default
  115. adds the a 'triples' attribute to the single DiGraph edge with a list of
  116. all triples between s and o.
  117. Also by default calculates the edge weight as the length of triples.
  118. Args:
  119. graph: a rdflib.Graph.
  120. calc_weights: If true calculate multi-graph edge-count as edge 'weight'
  121. edge_attrs: Callable to construct later edge_attributes. It receives
  122. 3 variables (s, p, o) and should construct a dictionary that is passed to
  123. networkx's add_edge(s, o, \*\*attrs) function.
  124. By default this will include setting the 'triples' attribute here,
  125. which is treated specially by us to be merged. Other attributes of
  126. multi-edges will only contain the attributes of the first edge.
  127. If you don't want the 'triples' attribute for tracking, set this to
  128. `lambda s, p, o: {}`.
  129. Returns: networkx.DiGraph
  130. Example:
  131. ```python
  132. >>> from rdflib import Graph, URIRef, Literal
  133. >>> g = Graph()
  134. >>> a, b, l = URIRef('a'), URIRef('b'), Literal('l')
  135. >>> p, q = URIRef('p'), URIRef('q')
  136. >>> edges = [(a, p, b), (a, q, b), (b, p, a), (b, p, l)]
  137. >>> for t in edges:
  138. ... g.add(t)
  139. ...
  140. >>> dg = rdflib_to_networkx_digraph(g)
  141. >>> dg[a][b]['weight']
  142. 2
  143. >>> sorted(dg[a][b]['triples']) == [(a, p, b), (a, q, b)]
  144. True
  145. >>> len(dg.edges())
  146. 3
  147. >>> dg.size()
  148. 3
  149. >>> dg.size(weight='weight')
  150. 4.0
  151. >>> dg = rdflib_to_networkx_graph(g, False, edge_attrs=lambda s,p,o:{})
  152. >>> 'weight' in dg[a][b]
  153. False
  154. >>> 'triples' in dg[a][b]
  155. False
  156. ```
  157. """
  158. import networkx as nx
  159. dg = nx.DiGraph()
  160. _rdflib_to_networkx_graph(graph, dg, calc_weights, edge_attrs, **kwds)
  161. return dg
  162. def rdflib_to_networkx_graph(
  163. graph: Graph,
  164. calc_weights: bool = True,
  165. edge_attrs=lambda s, p, o: {"triples": [(s, p, o)]},
  166. **kwds,
  167. ):
  168. r"""Converts the given graph into a networkx.Graph.
  169. As an [`rdflib.Graph()`][rdflib.Graph] can contain multiple directed edges between nodes, by
  170. default adds the a 'triples' attribute to the single DiGraph edge with a list of triples between s and o in graph.
  171. Also by default calculates the edge weight as the `len(triples)`.
  172. Args:
  173. graph: a rdflib.Graph.
  174. calc_weights: If true calculate multi-graph edge-count as edge 'weight'
  175. edge_attrs: Callable to construct later edge_attributes. It receives
  176. 3 variables (s, p, o) and should construct a dictionary that is
  177. passed to networkx's add_edge(s, o, \*\*attrs) function.
  178. By default this will include setting the 'triples' attribute here,
  179. which is treated specially by us to be merged. Other attributes of
  180. multi-edges will only contain the attributes of the first edge.
  181. If you don't want the 'triples' attribute for tracking, set this to
  182. `lambda s, p, o: {}`.
  183. Returns:
  184. networkx.Graph
  185. Example:
  186. ```python
  187. >>> from rdflib import Graph, URIRef, Literal
  188. >>> g = Graph()
  189. >>> a, b, l = URIRef('a'), URIRef('b'), Literal('l')
  190. >>> p, q = URIRef('p'), URIRef('q')
  191. >>> edges = [(a, p, b), (a, q, b), (b, p, a), (b, p, l)]
  192. >>> for t in edges:
  193. ... g.add(t)
  194. ...
  195. >>> ug = rdflib_to_networkx_graph(g)
  196. >>> ug[a][b]['weight']
  197. 3
  198. >>> sorted(ug[a][b]['triples']) == [(a, p, b), (a, q, b), (b, p, a)]
  199. True
  200. >>> len(ug.edges())
  201. 2
  202. >>> ug.size()
  203. 2
  204. >>> ug.size(weight='weight')
  205. 4.0
  206. >>> ug = rdflib_to_networkx_graph(g, False, edge_attrs=lambda s,p,o:{})
  207. >>> 'weight' in ug[a][b]
  208. False
  209. >>> 'triples' in ug[a][b]
  210. False
  211. ```
  212. """
  213. import networkx as nx
  214. g = nx.Graph()
  215. _rdflib_to_networkx_graph(graph, g, calc_weights, edge_attrs, **kwds)
  216. return g
  217. def rdflib_to_graphtool(
  218. graph: Graph,
  219. v_prop_names: List[str] = ["term"],
  220. e_prop_names: List[str] = ["term"],
  221. transform_s=lambda s, p, o: {"term": s},
  222. transform_p=lambda s, p, o: {"term": p},
  223. transform_o=lambda s, p, o: {"term": o},
  224. ):
  225. """Converts the given graph into a graph_tool.Graph().
  226. The subjects and objects are the later vertices of the Graph.
  227. The predicates become edges.
  228. Args:
  229. graph: a rdflib.Graph.
  230. v_prop_names: a list of names for the vertex properties. The default is set
  231. to ['term'] (see transform_s, transform_o below).
  232. e_prop_names: a list of names for the edge properties.
  233. transform_s: callable with s, p, o input. Should return a dictionary
  234. containing a value for each name in v_prop_names. By default is set
  235. to {'term': s} which in combination with v_prop_names = ['term']
  236. adds s as 'term' property to the generated vertex for s.
  237. transform_p: similar to transform_s, but wrt. e_prop_names. By default
  238. returns {'term': p} which adds p as a property to the generated
  239. edge between the vertex for s and the vertex for o.
  240. transform_o: similar to transform_s.
  241. Returns: graph_tool.Graph()
  242. Example:
  243. ```python
  244. >>> from rdflib import Graph, URIRef, Literal
  245. >>> g = Graph()
  246. >>> a, b, l = URIRef('a'), URIRef('b'), Literal('l')
  247. >>> p, q = URIRef('p'), URIRef('q')
  248. >>> edges = [(a, p, b), (a, q, b), (b, p, a), (b, p, l)]
  249. >>> for t in edges:
  250. ... g.add(t)
  251. ...
  252. >>> mdg = rdflib_to_graphtool(g)
  253. >>> len(list(mdg.edges()))
  254. 4
  255. >>> from graph_tool import util as gt_util
  256. >>> vpterm = mdg.vertex_properties['term']
  257. >>> va = gt_util.find_vertex(mdg, vpterm, a)[0]
  258. >>> vb = gt_util.find_vertex(mdg, vpterm, b)[0]
  259. >>> vl = gt_util.find_vertex(mdg, vpterm, l)[0]
  260. >>> (va, vb) in [(e.source(), e.target()) for e in list(mdg.edges())]
  261. True
  262. >>> epterm = mdg.edge_properties['term']
  263. >>> len(list(gt_util.find_edge(mdg, epterm, p))) == 3
  264. True
  265. >>> len(list(gt_util.find_edge(mdg, epterm, q))) == 1
  266. True
  267. >>> mdg = rdflib_to_graphtool(
  268. ... g,
  269. ... e_prop_names=[str('name')],
  270. ... transform_p=lambda s, p, o: {str('name'): unicode(p)})
  271. >>> epterm = mdg.edge_properties['name']
  272. >>> len(list(gt_util.find_edge(mdg, epterm, unicode(p)))) == 3
  273. True
  274. >>> len(list(gt_util.find_edge(mdg, epterm, unicode(q)))) == 1
  275. True
  276. ```
  277. """
  278. # pytype error: Can't find module 'graph_tool'.
  279. import graph_tool as gt # pytype: disable=import-error
  280. g = gt.Graph()
  281. vprops = [(vpn, g.new_vertex_property("object")) for vpn in v_prop_names]
  282. for vpn, vprop in vprops:
  283. g.vertex_properties[vpn] = vprop
  284. eprops = [(epn, g.new_edge_property("object")) for epn in e_prop_names]
  285. for epn, eprop in eprops:
  286. g.edge_properties[epn] = eprop
  287. node_to_vertex: Dict[Any, Any] = {}
  288. for s, p, o in graph:
  289. sv = node_to_vertex.get(s)
  290. if sv is None:
  291. v = g.add_vertex()
  292. node_to_vertex[s] = v
  293. tmp_props = transform_s(s, p, o)
  294. for vpn, vprop in vprops:
  295. vprop[v] = tmp_props[vpn]
  296. sv = v
  297. ov = node_to_vertex.get(o)
  298. if ov is None:
  299. v = g.add_vertex()
  300. node_to_vertex[o] = v
  301. tmp_props = transform_o(s, p, o)
  302. for vpn, vprop in vprops:
  303. vprop[v] = tmp_props[vpn]
  304. ov = v
  305. e = g.add_edge(sv, ov)
  306. tmp_props = transform_p(s, p, o)
  307. for epn, eprop in eprops:
  308. eprop[e] = tmp_props[epn]
  309. return g