20
$\begingroup$

Is there an easy way to access the Python library networkx from Mathematica?

The improvements to ExternalEvaluate in Mathematica 12.0 should make this feasible.

$\endgroup$
28
$\begingroup$

Mathematica 12.0 brings two new features that make this easier to do than it was before:

Below we implement a function nxFunction that automatically handles translating Mathematica expressions of interest to Python, as well as converting the results back. The usage will be

nxFunction["someNetworkxFunction"][graph, positionalArg2, "keyword1" -> keywordArgValue]

Here is a barebones example that serves as a proof of concept. (Improvements posted as additional answers are very welcome!)

Set up external session

First, make sure that the Python you are using has networkx installed, and start an external session. In the below example I am using an Anaconda virtualenv named "py37" on macOS. Adjust as necessary for your machine.

py = StartExternalSession[{"Python", 
   "Executable" -> AbsoluteFileName["~/anaconda/envs/py37/bin/python"]}]

Load the package:

ExternalEvaluate[py, "import networkx as nx"]

Mathematica -> Python conversion

We are going to use two Python helper function to translate arguments into the correct form. Most networkx functions that take a graph will take it as the first argument. This Python function takes a vertex list, an edge list and a graph type, and translates them to a networkx object. The rest of the arguments/options are passed as normal arguments / keyword arguments.

nxFun = ExternalFunction[py, "
  def nxfun(vertices, edges, gtype, fname, args, kwargs):
    fun = getattr(nx, fname)
    GraphClass = {'su': nx.Graph, 'sd': nx.DiGraph, 'mu': nx.MultiGraph, 'md': nx.MultiDiGraph}[gtype]
    g = GraphClass()
    g.add_nodes_from(vertices)
    g.add_edges_from(edges)
    return fun(g, *args, **kwargs)
  "]

The following is for calling networkx functions that do not take a graph argument:

nxPlainFun = ExternalFunction[py, "
  def nxplainfun(fname, args, kwargs):
    fun = getattr(nx, fname)
    return fun(*args, **kwargs)
  "]

Now we create Mathematica functions that call the above Python functions:

ClearAll[nxGraphQ]
nxGraphQ[_?MixedGraphQ] = False;
nxGraphQ[_?GraphQ] = True;
nxGraphQ[_] = False;

(* first argument is a graph *)
nxFunction[name_][g_?nxGraphQ, args___, kwargs : OptionsPattern[]] :=    
  nxFun[
  VertexList[g],
  List @@@ EdgeList[g],
  If[MultigraphQ[g],
   If[DirectedGraphQ[g], "md", "mu"],
   If[DirectedGraphQ[g], "sd", "su"]
   ],
  name,
  {args},
  Association[kwargs]
 ]

(* first argument is not a graph *)
nxFunction[name_][args___, kwargs : OptionsPattern[]] :=
 nxPlainFun[
  name, {args}, Association[kwargs]
 ]

Python -> Mathematica conversion

We create a custom serializer for networkx graphs, as described here:

ExternalEvaluate[py,
 "
 from wolframclient.language import wl
 from wolframclient.serializers import wolfram_encoder

 @wolfram_encoder.dispatch(nx.Graph)
 def encode_animal(serializer, graph):
     return serializer.encode(wl.Graph(graph.nodes, wl.Apply(wl.UndirectedEdge, graph.edges, [1])))

 @wolfram_encoder.dispatch(nx.DiGraph)
 def encode_animal(serializer, graph):
     return serializer.encode(wl.Graph(graph.nodes, wl.Apply(wl.DirectedEdge, graph.edges, [1])))
 "]

Try it out

Create a test graph:

SeedRandom[42]
g = RandomGraph[{10, 20}, DirectedEdges -> True]

Compute the betweenness:

nxFunction["betweenness_centrality"][g]
(* <|1 -> 0.256944, 2 -> 0.0416667, 3 -> 0., 4 -> 0.333333, 
 5 -> 0.0277778, 6 -> 0.236111, 7 -> 0.25, 8 -> 0.111111, 9 -> 0., 
 10 -> 0.0763889|> *)

Compute betweenness without normalization (and test keyword arguments):

nxFunction["betweenness_centrality"][g, "normalized" -> False]
(* <|1 -> 18.5, 2 -> 3., 3 -> 0., 4 -> 24., 5 -> 2., 6 -> 17., 
 7 -> 18., 8 -> 8., 9 -> 0., 10 -> 5.5|> *)

Compare with Mathematica's result:

BetweennessCentrality[g]
(* {18.5, 3., 0., 24., 2., 17., 18., 8., 0., 5.5} *)

A networkx function that returns a graph:

nxFunction["grid_graph"][{3, 4}]

enter image description here

Graph[nxFunction["margulis_gabber_galil_graph"][6], 
 VertexLabels -> Automatic]

enter image description here

nxFunction["hexagonal_lattice_graph"][6, 7]

enter image description here

Modify existing graphs:

nxFunction["ego_graph"][GridGraph[{5, 6}], 1, 3]

enter image description here

nxFunction["mycielskian"][GridGraph[{3, 3}]]

enter image description here

Compute minimal cycle basis:

nxFunction["minimum_cycle_basis"][GridGraph[{3, 4}]]

(* {{1, 2, 4, 5}, {2, 3, 5, 6}, {4, 5, 7, 8}, {5, 6, 8, 9}, {7, 8, 10, 11}, {8, 9, 11, 12}} *)

This is a first proof of concept. Improvement and suggestions are most welcome. I encourage everyone to post new answers either improving this one, or presenting independent approaches.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.