Skip to content

Commit f88ac0b

Browse files
committed
Misc fixes and adjustments
* RandomGraph now largely works (with some bugs still remaining) * graph options processing gone over and DRY'd a little * Code split up more: "algorithms.py" added and modules better documented. graph_generators.py -> generators.py
1 parent dc80054 commit f88ac0b

File tree

5 files changed

+196
-103
lines changed

5 files changed

+196
-103
lines changed

pymathics/graph/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
"""
66

77

8+
from pymathics.graph.version import __version__
89
from pymathics.graph.__main__ import *
910
from pymathics.graph.tree import *
10-
from pymathics.graph.graph_generators import *
11-
from pymathics.graph.version import __version__
11+
from pymathics.graph.generators import *
12+
from pymathics.graph.algorithms import *
1213

1314
pymathics_version_data = {
1415
"author": "The Mathics Team",

pymathics/graph/__main__.py

Lines changed: 49 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
# -*- coding: utf-8 -*-
22

33
"""
4-
Graphs
4+
Core routines for working with Graphs.
5+
A Graph is a tuple of a set of Nodes and Edges.
6+
7+
networkx does all the heavy lifting.
58
"""
69

710
# uses networkx
811

912
from mathics.builtin.base import Builtin, AtomBuiltin
1013
from mathics.builtin.graphics import GraphicsBox
11-
from mathics.builtin.randomnumbers import RandomEnv
1214
from mathics.core.expression import (
1315
Atom,
1416
Expression,
1517
Integer,
1618
Real,
1719
String,
1820
Symbol,
19-
from_python,
2021
)
2122
from mathics.builtin.patterns import Matcher
2223

2324
from inspect import isgenerator
2425

2526
WL_MARKER_TO_MATPLOTLIB = {
2627
"Circle": "o",
27-
"Diamond" :"D",
28-
"Square" :"s",
28+
"Diamond": "D",
29+
"Square": "s",
2930
"Star": "*",
3031
"Pentagon": "p",
3132
"Octagon": "8",
@@ -42,12 +43,42 @@
4243
"PlotLabel": "Null",
4344
"VertexLabels": "False",
4445
"VertexSize": "{}",
45-
"VertexShape": "o",
46+
"VertexShape": '"Circle"',
4647
"VertexStyle": "{}",
4748
}
4849

4950
import networkx as nx
5051

52+
53+
def has_directed_option(options: dict) -> bool:
54+
return options.get("System`DirectedEdges", False).to_python()
55+
56+
def _process_graph_options(g, options: dict) -> None:
57+
"""
58+
Handle common graph-related options like VertexLabels, PlotLabel, VertexShape, etc.
59+
"""
60+
# FIXME: for now we are adding both to both g and g.G.
61+
# g is where it is used in format. However we should wrap this as our object.
62+
# Access in G which might be better, currently isn't used.
63+
g.G.vertex_labels = g.vertex_labels = (
64+
options["System`VertexLabels"].to_python()
65+
if "System`VertexLabels" in options
66+
else False
67+
)
68+
shape = (
69+
options["System`VertexShape"].get_string_value()
70+
if "System`VertexShape" in options
71+
else "Circle"
72+
)
73+
74+
g.G.node_shape = g.node_shape = WL_MARKER_TO_MATPLOTLIB.get(shape, shape)
75+
g.G.title = g.title = (
76+
options["System`PlotLabel"].get_string_value()
77+
if "System`PlotLabel" in options
78+
else None
79+
)
80+
81+
5182
def _circular_layout(G):
5283
return nx.drawing.circular_layout(G, scale=1.5)
5384

@@ -495,12 +526,14 @@ def _graph_from_list(rules, options, new_vertices=None):
495526
return Graph(nx.Graph())
496527
else:
497528
new_edges, new_edge_properties = zip(*[_parse_item(x) for x in rules])
498-
return _create_graph(new_edges, new_edge_properties,
499-
options=options,
500-
new_vertices=new_vertices)
529+
return _create_graph(
530+
new_edges, new_edge_properties, options=options, new_vertices=new_vertices
531+
)
501532

502533

503-
def _create_graph(new_edges, new_edge_properties, options, from_graph=None, new_vertices=None):
534+
def _create_graph(
535+
new_edges, new_edge_properties, options, from_graph=None, new_vertices=None
536+
):
504537

505538
known_vertices = set()
506539
vertices = []
@@ -671,10 +704,7 @@ def full_new_edge_properties(new_edge_style):
671704
)
672705

673706
g = Graph(G)
674-
g.vertex_labels = G.vertex_labels = options["System`VertexLabels"]
675-
G.title = g.title = options["System`PlotLabel"]
676-
shape = options["System`VertexShape"].get_string_value()
677-
G.node_shape = g.vertex_shape = WL_MARKER_TO_MATPLOTLIB.get(shape, shape)
707+
_process_graph_options(g, options)
678708
return g
679709

680710

@@ -775,7 +805,9 @@ def apply(self, graph, evaluation, options):
775805

776806
def apply_1(self, vertices, edges, evaluation, options):
777807
"Graph[vertices_List, edges_List, OptionsPattern[%(name)s]]"
778-
return _graph_from_list(edges.leaves, options=options, new_vertices=vertices.leaves)
808+
return _graph_from_list(
809+
edges.leaves, options=options, new_vertices=vertices.leaves
810+
)
779811

780812

781813
class PathGraph(_NetworkXBuiltin):
@@ -1739,8 +1771,7 @@ def apply(self, graph, evaluation, options):
17391771
def degrees(graph):
17401772
degrees = dict(list(graph.G.degree(graph.vertices)))
17411773
return Expression(
1742-
"List",
1743-
*[Integer(degrees.get(v, 0)) for v in graph.vertices]
1774+
"List", *[Integer(degrees.get(v, 0)) for v in graph.vertices]
17441775
)
17451776

17461777
return self._evaluate_atom(graph, options, degrees)
@@ -1793,94 +1824,16 @@ def apply_s_t(self, graph, s, t, expression, evaluation, options):
17931824
return Expression("List")
17941825

17951826

1796-
class GraphDistance(_NetworkXBuiltin):
1797-
"""
1798-
>> GraphDistance[{1 <-> 2, 2 <-> 3, 3 <-> 4, 2 <-> 4, 4 -> 5}, 1, 5]
1799-
= 3
1800-
1801-
>> GraphDistance[{1 <-> 2, 2 <-> 3, 3 <-> 4, 4 -> 2, 4 -> 5}, 1, 5]
1802-
= 4
1803-
1804-
>> GraphDistance[{1 <-> 2, 2 <-> 3, 4 -> 3, 4 -> 2, 4 -> 5}, 1, 5]
1805-
= Infinity
1806-
1807-
>> GraphDistance[{1 <-> 2, 2 <-> 3, 3 <-> 4, 2 <-> 4, 4 -> 5}, 3]
1808-
= {2, 1, 0, 1, 2}
1809-
1810-
>> GraphDistance[{1 <-> 2, 3 <-> 4}, 3]
1811-
= {Infinity, Infinity, 0, 1}
1812-
1813-
#> GraphDistance[{}, 1, 1]
1814-
: The vertex at position 2 in GraphDistance[{}, 1, 1] does not belong to the graph at position 1.
1815-
= GraphDistance[{}, 1, 1]
1816-
#> GraphDistance[{1 -> 2}, 3, 4]
1817-
: The vertex at position 2 in GraphDistance[{1 -> 2}, 3, 4] does not belong to the graph at position 1.
1818-
= GraphDistance[{1 -> 2}, 3, 4]
1819-
"""
1820-
1821-
def apply_s(self, graph, s, expression, evaluation, options):
1822-
"%(name)s[graph_, s_, OptionsPattern[%(name)s]]"
1823-
graph = self._build_graph(graph, evaluation, options, expression)
1824-
if graph:
1825-
weight = graph.update_weights(evaluation)
1826-
d = nx.shortest_path_length(graph.G, source=s, weight=weight)
1827-
inf = Expression("DirectedInfinity", 1)
1828-
return Expression(
1829-
"List", *[d.get(v, inf) for v in graph.vertices.expressions]
1830-
)
1831-
1832-
def apply_s_t(self, graph, s, t, expression, evaluation, options):
1833-
"%(name)s[graph_, s_, t_, OptionsPattern[%(name)s]]"
1834-
graph = self._build_graph(graph, evaluation, options, expression)
1835-
if not graph:
1836-
return
1837-
G = graph.G
1838-
if not G.has_node(s):
1839-
self._not_a_vertex(expression, 2, evaluation)
1840-
elif not G.has_node(t):
1841-
self._not_a_vertex(expression, 3, evaluation)
1842-
else:
1843-
try:
1844-
weight = graph.update_weights(evaluation)
1845-
return from_python(
1846-
nx.shortest_path_length(graph.G, source=s, target=t, weight=weight)
1847-
)
1848-
except nx.exception.NetworkXNoPath:
1849-
return Expression("DirectedInfinity", 1)
1850-
1851-
18521827
def _convert_networkx_graph(G, options):
18531828
mapping = dict((v, Integer(i)) for i, v in enumerate(G.nodes))
18541829
G = nx.relabel_nodes(G, mapping)
18551830
edges = [Expression("System`UndirectedEdge", u, v) for u, v in G.edges]
18561831
return Graph(
18571832
G,
1858-
None,
1859-
options,
1833+
**options,
18601834
)
18611835

18621836

1863-
class RandomGraph(_NetworkXBuiltin):
1864-
def _generate(self, n, m, k, evaluation, options):
1865-
py_n = n.get_int_value()
1866-
py_m = m.get_int_value()
1867-
py_k = k.get_int_value()
1868-
1869-
with RandomEnv(evaluation) as rand:
1870-
for _ in range(py_k):
1871-
seed = rand.randint(0, 2 ** 63 - 1)
1872-
G = nx.gnm_random_graph(py_n, py_m, seed=seed)
1873-
yield _convert_networkx_graph(G, options)
1874-
1875-
def apply_nm(self, n, m, expression, evaluation, options):
1876-
"%(name)s[{n_Integer, m_Integer}, OptionsPattern[%(name)s]]"
1877-
return self._generate(n, m, Integer(1), evaluation, options)[0]
1878-
1879-
def apply_nm(self, n, m, k, expression, evaluation, options):
1880-
"%(name)s[{n_Integer, m_Integer}, k_Integer, OptionsPattern[%(name)s]]"
1881-
return Expression("List", *self._generate(n, m, k, evaluation, options))
1882-
1883-
18841837
class VertexAdd(_NetworkXBuiltin):
18851838
"""
18861839
>> g1 = Graph[{1 -> 2, 2 -> 3}];

pymathics/graph/algorithms.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Algorithms on Graphs.
4+
5+
networkx does all the heavy lifting.
6+
"""
7+
8+
from mathics.core.expression import Expression
9+
10+
from pymathics.graph.__main__ import (
11+
_NetworkXBuiltin,
12+
nx,
13+
)
14+
15+
from mathics.core.expression import (
16+
from_python,
17+
)
18+
19+
20+
21+
class GraphDistance(_NetworkXBuiltin):
22+
"""
23+
<dl>
24+
<dt>'GraphDistance[$g$, $s$, $t$]'
25+
<dd>returns the distance from source vertex $s$ to target vertex $t$ in the graph $g$.
26+
</dl>
27+
28+
<dl>
29+
<dt>'GraphDistance[$g$, $s$]'
30+
<dd>returns the distance from source vertex $s$ to all vertices in the graph $g$.
31+
</dl>
32+
33+
<dl>
34+
<dt>'GraphDistance[{$v$->$w$, ...}, ...]'
35+
<dd>use rules $v$->$w$ to specify the graph $g$
36+
</dl>
37+
38+
>> GraphDistance[{1 <-> 2, 2 <-> 3, 3 <-> 4, 2 <-> 4, 4 -> 5}, 1, 5]
39+
= 3
40+
41+
>> GraphDistance[{1 <-> 2, 2 <-> 3, 3 <-> 4, 4 -> 2, 4 -> 5}, 1, 5]
42+
= 4
43+
44+
>> GraphDistance[{1 <-> 2, 2 <-> 3, 4 -> 3, 4 -> 2, 4 -> 5}, 1, 5]
45+
= Infinity
46+
47+
>> GraphDistance[{1 <-> 2, 2 <-> 3, 3 <-> 4, 2 <-> 4, 4 -> 5}, 3]
48+
= {2, 1, 0, 1, 2}
49+
50+
>> GraphDistance[{1 <-> 2, 3 <-> 4}, 3]
51+
= {Infinity, Infinity, 0, 1}
52+
53+
#> GraphDistance[{}, 1, 1]
54+
: The vertex at position 2 in GraphDistance[{}, 1, 1] does not belong to the graph at position 1.
55+
= GraphDistance[{}, 1, 1]
56+
#> GraphDistance[{1 -> 2}, 3, 4]
57+
: The vertex at position 2 in GraphDistance[{1 -> 2}, 3, 4] does not belong to the graph at position 1.
58+
= GraphDistance[{1 -> 2}, 3, 4]
59+
"""
60+
61+
def apply_s(self, graph, s, expression, evaluation, options):
62+
"%(name)s[graph_, s_, OptionsPattern[%(name)s]]"
63+
graph = self._build_graph(graph, evaluation, options, expression)
64+
if graph:
65+
weight = graph.update_weights(evaluation)
66+
d = nx.shortest_path_length(graph.G, source=s, weight=weight)
67+
inf = Expression("DirectedInfinity", 1)
68+
return Expression(
69+
"List", *[d.get(v, inf) for v in graph.vertices]
70+
)
71+
72+
def apply_s_t(self, graph, s, t, expression, evaluation, options):
73+
"%(name)s[graph_, s_, t_, OptionsPattern[%(name)s]]"
74+
graph = self._build_graph(graph, evaluation, options, expression)
75+
if not graph:
76+
return
77+
G = graph.G
78+
if not G.has_node(s):
79+
self._not_a_vertex(expression, 2, evaluation)
80+
elif not G.has_node(t):
81+
self._not_a_vertex(expression, 3, evaluation)
82+
else:
83+
try:
84+
weight = graph.update_weights(evaluation)
85+
return from_python(
86+
nx.shortest_path_length(graph.G, source=s, target=t, weight=weight)
87+
)
88+
except nx.exception.NetworkXNoPath:
89+
return Expression("DirectedInfinity", 1)

0 commit comments

Comments
 (0)