|
1 | 1 | # -*- coding: utf-8 -*- |
2 | 2 |
|
3 | 3 | """ |
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. |
5 | 8 | """ |
6 | 9 |
|
7 | 10 | # uses networkx |
8 | 11 |
|
9 | 12 | from mathics.builtin.base import Builtin, AtomBuiltin |
10 | 13 | from mathics.builtin.graphics import GraphicsBox |
11 | | -from mathics.builtin.randomnumbers import RandomEnv |
12 | 14 | from mathics.core.expression import ( |
13 | 15 | Atom, |
14 | 16 | Expression, |
15 | 17 | Integer, |
16 | 18 | Real, |
17 | 19 | String, |
18 | 20 | Symbol, |
19 | | - from_python, |
20 | 21 | ) |
21 | 22 | from mathics.builtin.patterns import Matcher |
22 | 23 |
|
23 | 24 | from inspect import isgenerator |
24 | 25 |
|
25 | 26 | WL_MARKER_TO_MATPLOTLIB = { |
26 | 27 | "Circle": "o", |
27 | | - "Diamond" :"D", |
28 | | - "Square" :"s", |
| 28 | + "Diamond": "D", |
| 29 | + "Square": "s", |
29 | 30 | "Star": "*", |
30 | 31 | "Pentagon": "p", |
31 | 32 | "Octagon": "8", |
|
42 | 43 | "PlotLabel": "Null", |
43 | 44 | "VertexLabels": "False", |
44 | 45 | "VertexSize": "{}", |
45 | | - "VertexShape": "o", |
| 46 | + "VertexShape": '"Circle"', |
46 | 47 | "VertexStyle": "{}", |
47 | 48 | } |
48 | 49 |
|
49 | 50 | import networkx as nx |
50 | 51 |
|
| 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 | + |
51 | 82 | def _circular_layout(G): |
52 | 83 | return nx.drawing.circular_layout(G, scale=1.5) |
53 | 84 |
|
@@ -495,12 +526,14 @@ def _graph_from_list(rules, options, new_vertices=None): |
495 | 526 | return Graph(nx.Graph()) |
496 | 527 | else: |
497 | 528 | 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 | + ) |
501 | 532 |
|
502 | 533 |
|
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 | +): |
504 | 537 |
|
505 | 538 | known_vertices = set() |
506 | 539 | vertices = [] |
@@ -671,10 +704,7 @@ def full_new_edge_properties(new_edge_style): |
671 | 704 | ) |
672 | 705 |
|
673 | 706 | 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) |
678 | 708 | return g |
679 | 709 |
|
680 | 710 |
|
@@ -775,7 +805,9 @@ def apply(self, graph, evaluation, options): |
775 | 805 |
|
776 | 806 | def apply_1(self, vertices, edges, evaluation, options): |
777 | 807 | "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 | + ) |
779 | 811 |
|
780 | 812 |
|
781 | 813 | class PathGraph(_NetworkXBuiltin): |
@@ -1739,8 +1771,7 @@ def apply(self, graph, evaluation, options): |
1739 | 1771 | def degrees(graph): |
1740 | 1772 | degrees = dict(list(graph.G.degree(graph.vertices))) |
1741 | 1773 | 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] |
1744 | 1775 | ) |
1745 | 1776 |
|
1746 | 1777 | return self._evaluate_atom(graph, options, degrees) |
@@ -1793,94 +1824,16 @@ def apply_s_t(self, graph, s, t, expression, evaluation, options): |
1793 | 1824 | return Expression("List") |
1794 | 1825 |
|
1795 | 1826 |
|
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 | | - |
1852 | 1827 | def _convert_networkx_graph(G, options): |
1853 | 1828 | mapping = dict((v, Integer(i)) for i, v in enumerate(G.nodes)) |
1854 | 1829 | G = nx.relabel_nodes(G, mapping) |
1855 | 1830 | edges = [Expression("System`UndirectedEdge", u, v) for u, v in G.edges] |
1856 | 1831 | return Graph( |
1857 | 1832 | G, |
1858 | | - None, |
1859 | | - options, |
| 1833 | + **options, |
1860 | 1834 | ) |
1861 | 1835 |
|
1862 | 1836 |
|
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 | | - |
1884 | 1837 | class VertexAdd(_NetworkXBuiltin): |
1885 | 1838 | """ |
1886 | 1839 | >> g1 = Graph[{1 -> 2, 2 -> 3}]; |
|
0 commit comments