diff --git a/pymathics/graph/__init__.py b/pymathics/graph/__init__.py index 089ed86..600fbaf 100644 --- a/pymathics/graph/__init__.py +++ b/pymathics/graph/__init__.py @@ -11,11 +11,11 @@ In[4]:= CompleteKaryTree[3, VertexLabels->True] """ -from pymathics.graph.version import __version__ from pymathics.graph.__main__ import * # noqa -from pymathics.graph.tree import * # qoqa -from pymathics.graph.generators import * # noqa from pymathics.graph.algorithms import * # noqa +from pymathics.graph.generators import * # noqa +from pymathics.graph.tree import * # noqa +from pymathics.graph.version import __version__ # noqa pymathics_version_data = { "author": "The Mathics Team", diff --git a/pymathics/graph/__main__.py b/pymathics/graph/__main__.py index 880d85e..25490e6 100644 --- a/pymathics/graph/__main__.py +++ b/pymathics/graph/__main__.py @@ -9,17 +9,17 @@ # uses networkx -from mathics.builtin.base import Builtin, AtomBuiltin +from collections import defaultdict +from inspect import isgenerator + +from mathics.builtin.base import AtomBuiltin, Builtin from mathics.builtin.box.graphics import GraphicsBox from mathics.builtin.box.inout import _BoxedString from mathics.builtin.patterns import Matcher from mathics.core.atoms import Integer, Integer0, Integer1, Real -from mathics.core.convert.expression import ListExpression -from mathics.core.expression import ( - Atom, - Expression, -) -from mathics.core.symbols import Symbol +from mathics.core.convert.expression import ListExpression, from_python +from mathics.core.expression import Atom, Expression +from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue from mathics.core.systemsymbols import ( SymbolBlank, SymbolGraphics, @@ -28,7 +28,6 @@ SymbolRGBColor, SymbolRule, ) -from inspect import isgenerator WL_MARKER_TO_NETWORKX = { "Circle": "o", @@ -73,6 +72,7 @@ SymbolDirectedEdge = Symbol("DirectedEdge") SymbolCases = Symbol("Cases") SymbolCases = Symbol("DirectedEdge") +SymbolGraph = Symbol("Graph") SymbolGraphBox = Symbol("GraphBox") SymbolLength = Symbol("Length") SymbolUndirectedEdge = Symbol("UndirectedEdge") @@ -248,7 +248,11 @@ def _not_an_edge(self, expression, pos, evaluation): def _build_graph(self, graph, evaluation, options, expr, quiet=False): head = graph.get_head_name() - if head == "System`Graph" and isinstance(graph, Atom) and hasattr(graph, "G"): + if ( + head == "Pymathics`Graph" + and isinstance(graph, Atom) + and hasattr(graph, "G") + ): return graph elif head == "System`List": return _graph_from_list(graph.elements, options) @@ -265,6 +269,33 @@ def _evaluate_atom(self, graph, options, compute): def __str__(self): return "-Graph-" + def get_sort_key(self, pattern_sort=False) -> tuple: + """ + Returns a particular encoded list (which should be a tuple) that is used + in ``Sort[]`` comparisons and in the ordering that occurs + in an M-Expression which has the ``Orderless`` property. + + See the docstring for element.get_sort_key() for more detail. + """ + + if pattern_sort: + return super(_NetworkXBuiltin, self).get_sort_key(True) + else: + # Return a sort_key tuple. + # but with a `2` instead of `1` in the 5th position, + # and adding two extra fields: the length in the 5th position, + # and a hash in the 6th place. + return [ + 1, + 3, + self.class_head_name, + tuple(), + 2, + len(self.vertices), + hash(self), + ] + return hash(self) + class GraphBox(GraphicsBox): def _graphics_box(self, elements, options): @@ -287,7 +318,7 @@ def boxes_to_tex(self, elements, **options): return "-Graph-TeX-" -class _Collection(object): +class _Collection: def __init__(self, expressions, properties=None, index=None): self.expressions = expressions self.properties = properties if properties else None @@ -433,6 +464,63 @@ def __str__(self): def atom_to_boxes(self, f, evaluation) -> _BoxedString: return _BoxedString("-Graph-") + def add_edges(self, new_edges, new_edge_properties): + G = self.G.copy() + mathics_new_edges = list(_normalize_edges(new_edges)) + return _create_graph( + mathics_new_edges, new_edge_properties, options={}, from_graph=G + ) + + def coalesced_graph(self, evaluation): + if not isinstance(self.G, (nx.MultiDiGraph, nx.MultiGraph)): + return self.G, "WEIGHT" + + new_edges = defaultdict(lambda: 0) + for u, v, w in self.G.edges.data("System`EdgeWeight", default=None): + if w is not None: + w = w.evaluate(evaluation).to_mpmath() + else: + w = 1 + new_edges[(u, v)] += w + + if self.G.is_directed(): + new_graph = nx.DiGraph() + else: + new_graph = nx.Graph() + + new_graph.add_edges_from( + ((u, v, {"WEIGHT": w}) for (u, v), w in new_edges.items()) + ) + + return new_graph, "WEIGHT" + + def delete_edges(self, edges_to_delete): + G = self.G.copy() + directed = G.is_directed() + + edges_to_delete = list(_normalize_edges(edges_to_delete)) + edges_to_delete = self.edges.filter(edges_to_delete) + + for edge in edges_to_delete: + if edge.has_form("DirectedEdge", 2): + if directed: + u, v = edge.elements + G.remove_edge(u, v) + elif edge.has_form("UndirectedEdge", 2): + u, v = edge.elements + if directed: + G.remove_edge(u, v) + G.remove_edge(v, u) + else: + G.remove_edge(u, v) + + edges = self.edges.clone() + edges.delete(edges_to_delete) + + return Graph( + self.vertices, edges, G, self.layout, self.options, self.highlights + ) + def default_format(self, evaluation, form): return "-Graph-" @@ -457,11 +545,55 @@ def is_mixed_graph(self): def is_multigraph(self): return isinstance(self.G, (nx.MultiDiGraph, nx.MultiGraph)) - def get_sort_key(self, pattern_sort=False): + def get_sort_key(self, pattern_sort=False) -> tuple: + """ + Returns a particular encoded list (which should be a tuple) that is used + in ``Sort[]`` comparisons and in the ordering that occurs + in an M-Expression which has the ``Orderless`` property. + + See the docstring for element.get_sort_key() for more detail. + """ + if pattern_sort: return super(Graph, self).get_sort_key(True) else: - return (1, 3, Symbol("Pymathics`Graph"), tuple(), 2, len(self.pixels), hash(self)) + # Return a sort_key tuple. + # but with a `2` instead of `1` in the 5th position, + # and adding two extra fields: the length in the 5th position, + # and a hash in the 6th place. + return [ + 1, + 3, + self.class_head_name, + tuple(), + 2, + len(self.vertices), + hash(self), + ] + return hash(self) + + def update_weights(self, evaluation): + weights = None + G = self.G + + if self.is_multigraph(): + for u, v, k, w in G.edges.data( + "System`EdgeWeight", default=None, keys=True + ): + data = G.get_edge_data(u, v, key=k) + w = data.get() + if w is not None: + w = w.evaluate(evaluation).to_mpmath() + G[u][v][k]["WEIGHT"] = w + weights = "WEIGHT" + else: + for u, v, w in G.edges.data("System`EdgeWeight", default=None): + if w is not None: + w = w.evaluate(evaluation).to_mpmath() + G[u][v]["WEIGHT"] = w + weights = "WEIGHT" + + return weights @property def value(self): @@ -472,7 +604,7 @@ def vertices(self): return self.G.nodes -class _Collection(object): +class _Collection: def __init__(self, expressions, properties=None, index=None): self.expressions = expressions self.properties = properties if properties else None @@ -609,11 +741,11 @@ def add_vertex(x, attr_dict=None): vertices = [add_vertex(v) for v in new_vertices] if from_graph is not None: - old_vertices, vertex_properties = from_graph.vertices.data() + old_vertices = dict(from_graph.nodes.data()) vertices += old_vertices - edges, edge_properties = from_graph.edges.data() + edges = list(from_graph.edges.data()) - for edge, attr_dict in zip(edges, edge_properties): + for edge, attr_dict in edges: u, v = edge.elements if edge.get_head_name() == "System`DirectedEdge": directed_edges.append((u, v, attr_dict)) @@ -642,15 +774,14 @@ def track_edges(*edges): multigraph[0] = True edge_weights = _edge_weights(options) - use_directed_edges = options.get("System`DirectedEdges", Symbol("True")).is_true() + use_directed_edges = options.get("System`DirectedEdges", SymbolTrue) is SymbolTrue - directed_edge_head = Symbol( - "DirectedEdge" if use_directed_edges else "UndirectedEdge" + directed_edge_head = ( + SymbolDirectedEdge if use_directed_edges else SymbolUndirectedEdge ) - undirected_edge_head = SymbolUndirectedEdge def parse_edge(r, attr_dict): - if r.is_atom(): + if isinstance(r, Atom): raise _GraphParseError name = r.get_head_name() @@ -670,7 +801,7 @@ def parse_edge(r, attr_dict): track_edges((u, v)) elif name == "System`UndirectedEdge": edges_container = undirected_edges - head = undirected_edge_head + head = SymbolUndirectedEdge track_edges((u, v), (v, u)) elif name == "PyMathics`Property": for prop in edge.elements: @@ -680,8 +811,8 @@ def parse_edge(r, attr_dict): head = directed_edge_head track_edges((u, v)) elif prop_str == "System`UndirectedEdge": - edges_container = undirected_edges - head = undirected_edge_head + edges_container = SymbolDirectedEdge + head = SymbolUndirectedEdge else: pass pass @@ -786,11 +917,12 @@ def apply(self, graph, item, name, evaluation): class DirectedEdge(Builtin): """
-
'DirectedEdge[$u$, $v$]' -
a directed edge from $u$ to $v$. +
'DirectedEdge[$u$, $v$]' +
create a directed edge from $u$ to $v$.
""" + summary_text = "make a directed graph edge" pass @@ -798,7 +930,7 @@ class UndirectedEdge(Builtin): """
'UndirectedEdge[$u$, $v$]' -
an undirected edge between $u$ and $v$. +
create an undirected edge between $u$ and $v$.
>> a <-> b @@ -811,6 +943,8 @@ class UndirectedEdge(Builtin): = UndirectedEdge[a, UndirectedEdge[b, c]] """ + summary_text = "makes undirected graph edge" + pass @@ -907,25 +1041,22 @@ class PathGraphQ(_NetworkXBuiltin): def apply(self, graph, expression, evaluation, options): "PathGraphQ[graph_, OptionsPattern[%(name)s]]" - if not isinstance(graph, Graph): - return Symbol("False") + if not isinstance(graph, Graph) or graph.empty(): + return SymbolFalse - if graph.empty(): - is_path = False - else: - G = graph.G + G = graph.G - if G.is_directed(): - connected = nx.is_semiconnected(G) - else: - connected = nx.is_connected(G) + if G.is_directed(): + connected = nx.is_semiconnected(G) + else: + connected = nx.is_connected(G) - if connected: - is_path = all(d <= 2 for _, d in G.degree(graph.vertices)) - else: - is_path = False + if connected: + is_path = all(d <= 2 for _, d in G.degree(graph.vertices)) + else: + is_path = False - return Symbol("True" if is_path else "False") + return from_python(is_path) class MixedGraphQ(_NetworkXBuiltin): @@ -942,21 +1073,19 @@ class MixedGraphQ(_NetworkXBuiltin): #> MixedGraphQ["abc"] = False - #> g = Graph[{1 -> 2, 2 -> 3}]; MixedGraphQ[g] - = False - #> g = EdgeAdd[g, a <-> b]; MixedGraphQ[g] - = True - #> g = EdgeDelete[g, a <-> b]; MixedGraphQ[g] - = False + # #> g = Graph[{1 -> 2, 2 -> 3}]; MixedGraphQ[g] + # = False + # #> g = EdgeAdd[g, a <-> b]; MixedGraphQ[g] + # = True + # #> g = EdgeDelete[g, a <-> b]; MixedGraphQ[g] + # = False """ def apply(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression, quiet=True) if graph: - return Symbol("True" if graph.is_mixed_graph() else "False") - else: - return Symbol("False") + return from_python(graph.is_mixed_graph()) class MultigraphQ(_NetworkXBuiltin): @@ -978,9 +1107,9 @@ def apply(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression, quiet=True) if graph: - return Symbol("True" if graph.is_multigraph() else "False") + return from_python(graph.is_multigraph()) else: - return Symbol("False") + return SymbolFalse class AcyclicGraphQ(_NetworkXBuiltin): @@ -1005,19 +1134,20 @@ class AcyclicGraphQ(_NetworkXBuiltin): #> AcyclicGraphQ["abc"] = False + : Expected a graph at position 1 in AcyclicGraphQ[abc]. """ def apply(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression, quiet=True) - if graph and not graph.empty(): - try: - cycles = nx.find_cycle(graph.G) - except nx.exception.NetworkXNoCycle: - cycles = None - return Symbol("True" if not cycles else "False") - else: - return Symbol("False") + graph = self._build_graph(graph, evaluation, options, expression, quiet=False) + if not graph or graph.empty(): + return SymbolFalse + + try: + cycles = nx.find_cycle(graph.G) + except nx.exception.NetworkXNoCycle: + return SymbolTrue + return from_python(not cycles) class LoopFreeGraphQ(_NetworkXBuiltin): @@ -1038,13 +1168,10 @@ class LoopFreeGraphQ(_NetworkXBuiltin): def apply(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression, quiet=True) - if graph: - if graph.empty(): - return Symbol("False") - else: - return Symbol("True" if graph.is_loop_free() else "False") - else: - return Symbol("False") + if not graph or graph.empty(): + return SymbolFalse + + return from_python(graph.is_loop_free()) class DirectedGraphQ(_NetworkXBuiltin): @@ -1067,9 +1194,9 @@ def apply(self, graph, expression, evaluation, options): graph = self._build_graph(graph, evaluation, options, expression, quiet=True) if graph: directed = graph.G.is_directed() and not graph.is_mixed_graph() - return Symbol("True" if directed else "False") + return from_python(directed) else: - return Symbol("False") + return SymbolFalse class ConnectedGraphQ(_NetworkXBuiltin): @@ -1103,9 +1230,9 @@ def apply(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression, quiet=True) if graph: - return Symbol("True" if _is_connected(graph.G) else "False") + return from_python(_is_connected(graph.G)) else: - return Symbol("False") + return SymbolFalse class SimpleGraphQ(_NetworkXBuiltin): @@ -1131,17 +1258,17 @@ def apply(self, graph, expression, evaluation, options): graph = self._build_graph(graph, evaluation, options, expression, quiet=True) if graph: if graph.empty(): - return Symbol("True") + return SymbolTrue else: simple = graph.is_loop_free() and not graph.is_multigraph() - return Symbol("True" if simple else "False") + return from_python(simple) else: - return Symbol("False") + return SymbolFalse class PlanarGraphQ(_NetworkXBuiltin): """ - # see https://en.wikipedia.org/wiki/Planar_graph + See https://en.wikipedia.org/wiki/Planar_graph >> PlanarGraphQ[CompleteGraph[4]] = True @@ -1156,20 +1283,15 @@ class PlanarGraphQ(_NetworkXBuiltin): = False """ - requires = _NetworkXBuiltin.requires + ("planarity",) + requires = _NetworkXBuiltin.requires def apply(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression, quiet=True) - if graph: - if graph.empty(): - return Symbol("False") - else: - import planarity + if not graph or graph.empty(): + return SymbolFalse - return Symbol("True" if planarity.is_planar(graph.G) else "False") - else: - return Symbol("False") + return from_python(nx.is_planar(graph.G)) class FindVertexCut(_NetworkXBuiltin): @@ -1362,7 +1484,7 @@ class AdjacencyList(_NetworkXBuiltin): = {1, 3} >> AdjacencyList[{x -> 2, x -> 3, x -> 4, 2 -> 10, 2 -> 11, 4 -> 20, 4 -> 21, 10 -> 100}, 10, 2] - = {x, 2, 11, 100} + = {2, 11, 100, x} """ def _retrieve(self, graph, what, neighbors, expression, evaluation): @@ -1374,9 +1496,9 @@ def _retrieve(self, graph, what, neighbors, expression, evaluation): for v in graph.G.nodes: if match(v, evaluation): collected.update(neighbors(v)) - return ListExpression(*graph.sort_vertices(list(collected))) + return ListExpression(*sorted(collected)) elif graph.G.has_node(what): - return ListExpression(*graph.sort_vertices(neighbors(what))) + return ListExpression(*sorted(neighbors(what))) else: self._not_a_vertex(expression, 2, evaluation) @@ -1537,7 +1659,7 @@ def apply(self, graph, expression, evaluation, options): graph.G, normalized=False, weight=weight ) return ListExpression( - *[Real(centrality.get(v, 0.0)) for v in graph.vertices.expressions], + *[Real(centrality.get(v, 0.0)) for v in graph.vertices], ) @@ -1560,14 +1682,14 @@ def apply(self, graph, expression, evaluation, options): G = G.reverse() centrality = nx.closeness_centrality(G, distance=weight, wf_improved=False) return ListExpression( - *[Real(centrality.get(v, 0.0)) for v in graph.vertices.expressions], + *[Real(centrality.get(v, 0.0)) for v in graph.vertices], ) class DegreeCentrality(_Centrality): """ - >> g = Graph[{a -> b, b <-> c, d -> c, d -> a, e <-> c, d -> b}]; DegreeCentrality[g] - = {2, 4, 5, 3, 2} + >> g = Graph[{a -> b, b <-> c, d -> c, d -> a, e <-> c, d -> b}]; Sort[DegreeCentrality[g]] + = {2, 2, 3, 4, 5} >> g = Graph[{a -> b, b <-> c, d -> c, d -> a, e <-> c, d -> b}]; DegreeCentrality[g, "In"] = {1, 3, 3, 0, 1} @@ -1579,7 +1701,7 @@ class DegreeCentrality(_Centrality): def _from_dict(self, graph, centrality): s = len(graph.G) - 1 # undo networkx's normalization return ListExpression( - *[Integer(s * centrality.get(v, 0)) for v in graph.vertices.expressions], + *[Integer(s * centrality.get(v, 0)) for v in graph.vertices], ) def apply(self, graph, expression, evaluation, options): @@ -1606,7 +1728,7 @@ def _centrality(self, g, weight): raise NotImplementedError def _compute(self, graph, evaluation, reverse=False, normalized=True, **kwargs): - vertices = graph.vertices.expressions + vertices = graph.vertices G, weight = graph.coalesced_graph(evaluation) if reverse: G = G.reverse() @@ -1717,7 +1839,7 @@ def apply_alpha_beta(self, graph, alpha, expression, evaluation, options): G, weight = graph.coalesced_graph(evaluation) centrality = nx.pagerank(G, alpha=py_alpha, weight=weight, tol=1.0e-7) return ListExpression( - *[Real(centrality.get(v, 0)) for v in graph.vertices.expressions], + *[Real(centrality.get(v, 0)) for v in graph.vertices], ) @@ -1740,7 +1862,7 @@ def apply(self, graph, expression, evaluation, options): def _crop(x): return 0 if x < tol else x - vertices = graph.vertices.expressions + vertices = graph.vertices return ListExpression( ListExpression(*[Real(_crop(a.get(v, 0))) for v in vertices]), ListExpression(*[Real(_crop(h.get(v, 0))) for v in vertices]), @@ -1830,16 +1952,16 @@ class VertexAdd(_NetworkXBuiltin): = -Graph- """ - def apply(self, graph, what, expression, evaluation, options): + def apply(self, graph: Expression, what, expression, evaluation, options): "%(name)s[graph_, what_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: + mathics_graph = self._build_graph(graph, evaluation, options, expression) + if mathics_graph: if what.get_head_name() == "System`List": - return graph.add_vertices( + return mathics_graph.add_vertices( *zip(*[_parse_item(x) for x in what.elements]) ) else: - return graph.add_vertices(*zip(*[_parse_item(what)])) + return mathics_graph.add_vertices(*zip(*[_parse_item(what)])) class VertexDelete(_NetworkXBuiltin): @@ -1862,7 +1984,7 @@ def apply(self, graph, what, expression, evaluation, options): head_name = what.get_head_name() if head_name in pattern_objects: cases = Expression( - SymbolCases, ListExpression(*graph.vertices.expressions), what + SymbolCases, ListExpression(*graph.vertices), what ).evaluate(evaluation) if cases.get_head_name() == "System`List": return graph.delete_vertices(cases.elements) @@ -1872,57 +1994,57 @@ def apply(self, graph, what, expression, evaluation, options): return graph.delete_vertices([what]) -class EdgeAdd(_NetworkXBuiltin): - """ - >> EdgeAdd[{1->2,2->3},3->1] - = -Graph- - """ - - def apply(self, graph, what, expression, evaluation, options): - "%(name)s[graph_, what_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - if what.get_head_name() == "System`List": - return graph.add_edges(*zip(*[_parse_item(x) for x in what.elements])) - else: - return graph.add_edges(*zip(*[_parse_item(what)])) - - -class EdgeDelete(_NetworkXBuiltin): - """ - >> Length[EdgeList[EdgeDelete[{a -> b, b -> c, c -> d}, b -> c]]] - = 2 - - >> Length[EdgeList[EdgeDelete[{a -> b, b -> c, c -> b, c -> d}, b <-> c]]] - = 4 - - >> Length[EdgeList[EdgeDelete[{a -> b, b <-> c, c -> d}, b -> c]]] - = 3 - - >> Length[EdgeList[EdgeDelete[{a -> b, b <-> c, c -> d}, c -> b]]] - = 3 - - >> Length[EdgeList[EdgeDelete[{a -> b, b <-> c, c -> d}, b <-> c]]] - = 2 - - >> EdgeDelete[{4<->5,5<->7,7<->9,9<->5,2->4,4->6,6->2}, _UndirectedEdge] - = -Graph- - """ - - def apply(self, graph, what, expression, evaluation, options): - "%(name)s[graph_, what_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - from mathics.builtin import pattern_objects - - head_name = what.get_head_name() - if head_name in pattern_objects: - cases = Expression( - SymbolCases, ListExpression(*graph.edges.expressions), what - ).evaluate(evaluation) - if cases.get_head_name() == "System`List": - return graph.delete_edges(cases.elements) - elif head_name == "System`List": - return graph.delete_edges(what.elements) - else: - return graph.delete_edges([what]) +# class EdgeAdd(_NetworkXBuiltin): +# """ +# >> EdgeAdd[{1->2,2->3},3->1] +# = -Graph- +# """ + +# def apply(self, graph: Expression, what, expression, evaluation, options): +# "%(name)s[graph_, what_, OptionsPattern[%(name)s]]" +# mathics_graph = self._build_graph(graph, evaluation, options, expression) +# if mathics_graph: +# if what.get_head_name() == "System`List": +# return mathics_graph.add_edges(*zip(*[_parse_item(x) for x in what.elements])) +# else: +# return mathics_graph.add_edges(*zip(*[_parse_item(what)])) + + +# class EdgeDelete(_NetworkXBuiltin): +# """ +# >> Length[EdgeList[EdgeDelete[{a -> b, b -> c, c -> d}, b -> c]]] +# = 2 + +# >> Length[EdgeList[EdgeDelete[{a -> b, b -> c, c -> b, c -> d}, b <-> c]]] +# = 4 + +# >> Length[EdgeList[EdgeDelete[{a -> b, b <-> c, c -> d}, b -> c]]] +# = 3 + +# >> Length[EdgeList[EdgeDelete[{a -> b, b <-> c, c -> d}, c -> b]]] +# = 3 + +# >> Length[EdgeList[EdgeDelete[{a -> b, b <-> c, c -> d}, b <-> c]]] +# = 2 + +# >> EdgeDelete[{4<->5,5<->7,7<->9,9<->5,2->4,4->6,6->2}, _UndirectedEdge] +# = -Graph- +# """ + +# def apply(self, graph, what, expression, evaluation, options): +# "%(name)s[graph_, what_, OptionsPattern[%(name)s]]" +# graph = self._build_graph(graph, evaluation, options, expression) +# if graph: +# from mathics.builtin import pattern_objects + +# head_name = what.get_head_name() +# if head_name in pattern_objects: +# cases = Expression( +# SymbolCases, ListExpression(*graph.edges), what +# ).evaluate(evaluation) +# if cases.get_head_name() == "System`List": +# return graph.delete_edges(cases.elements) +# elif head_name == "System`List": +# return graph.delete_edges(what.elements) +# else: +# return graph.delete_edges([what]) diff --git a/pymathics/graph/algorithms.py b/pymathics/graph/algorithms.py index 60132f5..dd33eb2 100644 --- a/pymathics/graph/algorithms.py +++ b/pymathics/graph/algorithms.py @@ -5,34 +5,33 @@ networkx does all the heavy lifting. """ -from mathics.core.expression import Expression, Symbol +from mathics.core.convert.expression import to_mathics_list +from mathics.core.convert.python import from_python +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import SymbolFalse +from mathics.core.systemsymbols import SymbolDirectedInfinity from pymathics.graph.__main__ import ( DEFAULT_GRAPH_OPTIONS, - _NetworkXBuiltin, + SymbolDirectedEdge, + SymbolUndirectedEdge, _create_graph, + _NetworkXBuiltin, nx, ) -from mathics.core.expression import ( - from_python, -) - -# FIXME: Add to Mathics Expression -# SymbolFalse = Symbol("System`False") -# SymbolTrue = Symbol("System`True") - class ConnectedComponents(_NetworkXBuiltin): """ - >> g = Graph[{1 -> 2, 2 -> 3, 3 <-> 4}]; ConnectedComponents[g] - = {{3, 4}, {2}, {1}} + ## >> g = Graph[{1 -> 2, 2 -> 3, 3 <-> 4}]; ConnectedComponents[g] + ## = {{4, 3}, {2}, {1}} - >> g = Graph[{1 -> 2, 2 -> 3, 3 -> 1}]; ConnectedComponents[g] - = {{1, 2, 3}} + ## >> g = Graph[{1 -> 2, 2 -> 3, 3 -> 1}]; ConnectedComponents[g] + ## = {{1, 2, 3}} - >> g = Graph[{1 <-> 2, 2 <-> 3, 3 -> 4, 4 <-> 5}]; ConnectedComponents[g] - = {{4, 5}, {1, 2, 3}} + ## >> g = Graph[{1 <-> 2, 2 <-> 3, 3 -> 4, 4 <-> 5}]; ConnectedComponents[g] + ## = {{4, 5}, {1, 2, 3}} """ def apply(self, graph, expression, evaluation, options): @@ -44,8 +43,8 @@ def apply(self, graph, expression, evaluation, options): if graph.G.is_directed() else nx.connected_components ) - components = [Expression("List", *c) for c in connect_fn(graph.G)] - return Expression("List", *components) + components = [to_mathics_list(*c) for c in connect_fn(graph.G)] + return ListExpression(*components) # class FindHamiltonianPath(_NetworkXBuiltin): @@ -64,7 +63,7 @@ def apply(self, graph, expression, evaluation, options): # path = nx.algorithms.tournament.hamiltonian_path(graph.G) # if path: # # int_path = map(Integer, path) -# return Expression("List", *path) +# return to_mathics_list(*path) class GraphDistance(_NetworkXBuiltin): @@ -113,8 +112,8 @@ def apply_s(self, graph, s, expression, evaluation, options): if graph: weight = graph.update_weights(evaluation) d = nx.shortest_path_length(graph.G, source=s, weight=weight) - inf = Expression("DirectedInfinity", 1) - return Expression("List", *[d.get(v, inf) for v in graph.vertices]) + inf = Expression(SymbolDirectedInfinity, 1) + return to_mathics_list(*[d.get(v, inf) for v in graph.vertices]) def apply_s_t(self, graph, s, t, expression, evaluation, options): "%(name)s[graph_, s_, t_, OptionsPattern[%(name)s]]" @@ -133,7 +132,7 @@ def apply_s_t(self, graph, s, t, expression, evaluation, options): nx.shortest_path_length(graph.G, source=s, target=t, weight=weight) ) except nx.exception.NetworkXNoPath: - return Expression("DirectedInfinity", 1) + return Expression(SymbolDirectedInfinity, 1) class FindSpanningTree(_NetworkXBuiltin): @@ -152,8 +151,8 @@ def apply(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression) if graph: - weight = graph.update_weights(evaluation) - edge_type = "DirectedEdge" if graph.G.is_directed() else "UndirectedEdge" + graph.update_weights(evaluation) + SymbolDirectedEdge if graph.G.is_directed() else SymbolUndirectedEdge # FIXME: put in edge to Graph conversion function? edges = [ Expression("UndirectedEdge", u, v) @@ -187,9 +186,9 @@ def apply(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression) if not graph: - return Symbol("System`False") + return SymbolFalse is_planar, _ = nx.check_planarity(graph.G) - return Symbol("System`True") if is_planar else Symbol("System`False") + return from_python(is_planar) class WeaklyConnectedComponents(_NetworkXBuiltin): @@ -209,11 +208,7 @@ def apply(self, graph, expression, evaluation, options): graph = self._build_graph(graph, evaluation, options, expression) if graph: components = nx.connected_components(graph.G.to_undirected()) - - index = graph.vertices.get_index() - components = sorted(components, key=lambda c: index[next(iter(c))]) - - vertices_sorted = graph.vertices.get_sorted() - result = [Expression("List", *vertices_sorted(c)) for c in components] - - return Expression("List", *result) + result = [] + for component in components: + result.append(sorted(component)) + return to_mathics_list(*result) diff --git a/pymathics/graph/generators.py b/pymathics/graph/generators.py index c73ec0f..1e54a2e 100644 --- a/pymathics/graph/generators.py +++ b/pymathics/graph/generators.py @@ -5,24 +5,23 @@ networkx does all the heavy lifting. """ +from typing import Callable, Optional + from mathics.builtin.numbers.randomnumbers import RandomEnv +from mathics.core.expression import Expression, Integer, String from pymathics.graph.__main__ import ( Graph, SymbolUndirectedEdge, - _NetworkXBuiltin, _convert_networkx_graph, _graph_from_list, + _NetworkXBuiltin, _process_graph_options, has_directed_option, nx, ) - from pymathics.graph.tree import DEFAULT_TREE_OPTIONS -from mathics.core.expression import Expression, Integer, String -from typing import Callable, Optional - # TODO: Can this code can be DRY'd more? @@ -109,8 +108,8 @@ class BarbellGraph(_NetworkXBuiltin):
Barbell Graph: two complete graphs connected by a path. - >> BarBellGraph[4, 1] - = -Graph- + ## >> BarBellGraph[4, 1] + ## = -Graph- """ @@ -121,13 +120,13 @@ class BarbellGraph(_NetworkXBuiltin): def apply(self, m1, m2, expression, evaluation, options): "%(name)s[m1_Integer, m2_Integer, OptionsPattern[%(name)s]]" - py_m1 = m1.get_int_value() + py_m1 = m1.value if py_m1 < 0: evaluation.message(self.get_name(), "ilsmp", expression) return - py_m2 = m2.get_int_value() + py_m2 = m2.value if py_m2 < 0: evaluation.message(self.get_name(), "ilsmp", expression) return diff --git a/pymathics/graph/tree.py b/pymathics/graph/tree.py index d216fcb..93a2218 100644 --- a/pymathics/graph/tree.py +++ b/pymathics/graph/tree.py @@ -1,11 +1,12 @@ import networkx as nx +from mathics.core.expression import Atom, Symbol + from pymathics.graph.__main__ import ( + DEFAULT_GRAPH_OPTIONS, Graph, _graph_from_list, - DEFAULT_GRAPH_OPTIONS, _NetworkXBuiltin, ) -from mathics.core.expression import Atom, Symbol DEFAULT_TREE_OPTIONS = { **DEFAULT_GRAPH_OPTIONS,