diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7841028..9743ead 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,15 +9,20 @@ repos: stages: [commit] - id: end-of-file-fixer stages: [commit] - - id: trailing-whitespace +# - repo: https://github.com/pycqa/isort +# rev: 5.10.1 +# hooks: +# - id: isort +# stages: [commit] - repo: https://github.com/psf/black rev: 22.3.0 hooks: - id: black language_version: python3 exclude: 'pymathics/graph/version.py' -- repo: https://github.com/pycqa/flake8 - rev: 3.9.2 - hooks: - - id: flake8 - stages: [commit] + stages: [commit] +# - repo: https://github.com/pycqa/flake8 +# rev: 3.9.2 +# hooks: +# - id: flake8 +# stages: [commit] diff --git a/Makefile b/Makefile index 6dea07b..346b6fa 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,8 @@ # remake --tasks to shows the targets and the comments GIT2CL ?= admin-tools/git2cl -PYTHON ?= python3 -PIP ?= pip3 +PYTHON ?= python +PIP ?= $(PYTHON) -m pip RM ?= rm .PHONY: all build \ @@ -40,7 +40,7 @@ install: pypi-setup $(PYTHON) setup.py install # Run tests -check: pytest doctest +test check: pytest #: Remove derived files clean: clean-pyc @@ -51,7 +51,7 @@ clean-pyc: #: Run py.test tests. Use environment variable "o" for pytest options pytest: - py.test test $o + $(PYTHON) -m pytest test $o # #: Create data that is used to in Django docs and to build TeX PDF diff --git a/README.rst b/README.rst index 9e7d621..d9c49c5 100644 --- a/README.rst +++ b/README.rst @@ -8,13 +8,13 @@ Example Session :: $ mathicsscript - Mathicscript: 5.0.0, Mathics 5.0.0 + Mathicscript: 5.0.0, Mathics 6.0.0 on CPython 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] using SymPy 1.9, mpmath 1.2.1, numpy 1.21.5 matplotlib 3.5.2, Asymptote version 2.81 - Copyright (C) 2011-2022 The Mathics Team. + Copyright (C) 2011-2023 The Mathics3 Team. This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. @@ -23,7 +23,6 @@ Example Session Quit by pressing CONTROL-D In[1]:= LoadModule["pymathics.graph"] - Out[1]= pymathics.graph In[2]:= BinomialTree[3] In[3]:= BinomialTree[6] diff --git a/pymathics/graph/__init__.py b/pymathics/graph/__init__.py index 600fbaf..4bb73a0 100644 --- a/pymathics/graph/__init__.py +++ b/pymathics/graph/__init__.py @@ -1,7 +1,7 @@ -"""Pymathics Graph - Working with Graphs (Vertices and Edges) +""" +Graphs - Vertices and Edges -This module provides functions and variables for workting with -graphs. +A Pymathics module that provides functions and variables for working with graphs. Example: In[1]:= LoadModule["pymathics.graph"] @@ -9,16 +9,56 @@ In[2]:= BinomialTree[3] In[3]:= BinomialTree[6] In[4]:= CompleteKaryTree[3, VertexLabels->True] + +Networkx does the heavy lifting here. """ -from pymathics.graph.__main__ import * # noqa +from pymathics.graph.base import ( + AcyclicGraphQ, + BetweennessCentrality, + ClosenessCentrality, + ConnectedGraphQ, + DegreeCentrality, + DirectedEdge, + DirectedGraphQ, + EdgeConnectivity, + EdgeIndex, + EdgeList, + EdgeRules, + EigenvectorCentrality, + FindShortestPath, + FindVertexCut, + Graph, + GraphBox, + HITSCentrality, + HighlightGraph, + KatzCentrality, + LoopFreeGraphQ, + MixedGraphQ, + MultigraphQ, + PageRankCentrality, + PlanarGraphQ, + PathGraphQ, + Property, + PropertyValue, + SimpleGraphQ, + UndirectedEdge, + VertexAdd, + VertexConnectivity, + VertexDelete, + VertexIndex, + VertexList, +) + +from pymathics.graph.measures_and_metrics import EdgeCount, VertexCount, VertexDegree + 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", + "author": "The Mathics3 Team", "version": __version__, "name": "Graph", "requires": ["networkx"], diff --git a/pymathics/graph/algorithms.py b/pymathics/graph/algorithms.py index dd33eb2..421dda5 100644 --- a/pymathics/graph/algorithms.py +++ b/pymathics/graph/algorithms.py @@ -1,18 +1,19 @@ # -*- coding: utf-8 -*- """ -Algorithms on Graphs. - -networkx does all the heavy lifting. +Algorithms on Graphs """ +from typing import Optional + from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_python +from mathics.core.evaluation import Evaluation 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 ( +from pymathics.graph.base import ( DEFAULT_GRAPH_OPTIONS, SymbolDirectedEdge, SymbolUndirectedEdge, @@ -25,7 +26,7 @@ class ConnectedComponents(_NetworkXBuiltin): """ ## >> g = Graph[{1 -> 2, 2 -> 3, 3 <-> 4}]; ConnectedComponents[g] - ## = {{4, 3}, {2}, {1}} + ## = {{3, 4}, {2}, {1}} ## >> g = Graph[{1 -> 2, 2 -> 3, 3 -> 1}]; ConnectedComponents[g] ## = {{1, 2, 3}} @@ -34,7 +35,9 @@ class ConnectedComponents(_NetworkXBuiltin): ## = {{4, 5}, {1, 2, 3}} """ - def apply(self, graph, expression, evaluation, options): + def eval( + self, graph, expression, evaluation: Evaluation, options: dict + ) -> Optional[ListExpression]: "ConnectedComponents[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression) if graph: @@ -54,10 +57,10 @@ def apply(self, graph, expression, evaluation, options): #
returns a Hamiltonian path in the given tournament graph. # # """ -# def apply_(self, graph, expression, evaluation, options): -# "%(name)s[graph_, OptionsPattern[%(name)s]]" +# def eval_(self, graph, expression, evaluation: Evaluation, options): +# "FindHamiltonianPath[graph_, OptionsPattern[FindHamiltonPath]]" -# graph = self._build_graph(graph, evaluation, options, expression) +# graph = self._build_graph(graph, evaluation: Evaluation, options, expression) # if graph: # # FIXME: for this to work we need to fill in all O(n^2) edges as an adjacency matrix? # path = nx.algorithms.tournament.hamiltonian_path(graph.G) @@ -106,8 +109,10 @@ class GraphDistance(_NetworkXBuiltin): = GraphDistance[{1 -> 2}, 3, 4] """ - def apply_s(self, graph, s, expression, evaluation, options): - "%(name)s[graph_, s_, OptionsPattern[%(name)s]]" + def eval_s( + self, graph, s, expression, evaluation: Evaluation, options: dict + ) -> Optional[ListExpression]: + "GraphDistance[graph_, s_, OptionsPattern[GraphDistance]]" graph = self._build_graph(graph, evaluation, options, expression) if graph: weight = graph.update_weights(evaluation) @@ -115,8 +120,8 @@ def apply_s(self, graph, s, expression, evaluation, options): 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]]" + def eval_s_t(self, graph, s, t, expression, evaluation: Evaluation, options: dict): + "GraphDistance[graph_, s_, t_, OptionsPattern[GraphDistance]]" graph = self._build_graph(graph, evaluation, options, expression) if not graph: return @@ -147,7 +152,7 @@ class FindSpanningTree(_NetworkXBuiltin): options = DEFAULT_GRAPH_OPTIONS - def apply(self, graph, expression, evaluation, options): + def eval(self, graph, expression, evaluation: Evaluation, options: dict): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression) if graph: @@ -182,8 +187,8 @@ class PlanarGraphQ(_NetworkXBuiltin): options = DEFAULT_GRAPH_OPTIONS - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" + def eval(self, graph, expression, evaluation: Evaluation, options: dict): + "PlanarGraphQ[graph_, OptionsPattern[PlanarGraphQ]]" graph = self._build_graph(graph, evaluation, options, expression) if not graph: return SymbolFalse @@ -203,8 +208,8 @@ class WeaklyConnectedComponents(_NetworkXBuiltin): = {{1, 2, 3, 4, 5}, {6, 7, 8}} """ - def apply(self, graph, expression, evaluation, options): - "WeaklyConnectedComponents[graph_, OptionsPattern[%(name)s]]" + def eval(self, graph, expression, evaluation: Evaluation, options): + "WeaklyConnectedComponents[graph_, OptionsPattern[WeaklyConnectedComponents]]" graph = self._build_graph(graph, evaluation, options, expression) if graph: components = nx.connected_components(graph.G.to_undirected()) diff --git a/pymathics/graph/__main__.py b/pymathics/graph/base.py similarity index 92% rename from pymathics/graph/__main__.py rename to pymathics/graph/base.py index 2095bb4..d1048d3 100644 --- a/pymathics/graph/__main__.py +++ b/pymathics/graph/base.py @@ -14,11 +14,9 @@ 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.atoms import Atom, Integer, Integer0, Integer1, Real from mathics.core.convert.expression import ListExpression, from_python -from mathics.core.expression import Atom, Expression +from mathics.core.expression import Expression from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue from mathics.core.systemsymbols import ( SymbolBlank, @@ -29,6 +27,8 @@ SymbolRGBColor, SymbolRule, ) +from mathics.eval.makeboxes import _boxed_string +from mathics.eval.patterns import Matcher WL_MARKER_TO_NETWORKX = { "Circle": "o", @@ -71,11 +71,9 @@ import networkx as nx SymbolDirectedEdge = Symbol("DirectedEdge") -SymbolCases = Symbol("Cases") SymbolCases = Symbol("DirectedEdge") SymbolGraph = Symbol("Graph") SymbolGraphBox = Symbol("GraphBox") -SymbolLength = Symbol("Length") SymbolUndirectedEdge = Symbol("UndirectedEdge") @@ -207,6 +205,16 @@ def _components(G): return nx.connected_components(G) +def _convert_networkx_graph(G, options): + mapping = dict((v, Integer(i)) for i, v in enumerate(G.nodes)) + G = nx.relabel_nodes(G, mapping) + [Expression(SymbolUndirectedEdge, u, v) for u, v in G.edges] + return Graph( + G, + **options, + ) + + _default_minimum_distance = 0.3 @@ -298,27 +306,6 @@ def get_sort_key(self, pattern_sort=False) -> tuple: return hash(self) -class GraphBox(GraphicsBox): - def _graphics_box(self, elements, options): - evaluation = options["evaluation"] - graph, form = elements - primitives = graph._layout(evaluation) - graphics = Expression(SymbolGraphics, primitives) - graphics_box = Expression(SymbolMakeBoxes, graphics, form).evaluate(evaluation) - return graphics_box - - def boxes_to_text(self, elements, **options): - return "-Graph-" - - def boxes_to_xml(self, elements, **options): - # Figure out what to do here. - return "-Graph-XML-" - - def boxes_to_tex(self, elements, **options): - # Figure out what to do here. - return "-Graph-TeX-" - - class _Collection: def __init__(self, expressions, properties=None, index=None): self.expressions = expressions @@ -447,6 +434,98 @@ def _normalize_edges(edges): yield edge +class _Collection: + def __init__(self, expressions, properties=None, index=None): + self.expressions = expressions + self.properties = properties if properties else None + self.index = index + + def clone(self): + properties = self.properties + return _Collection( + self.expressions[:], properties[:] if properties else None, None + ) + + def filter(self, expressions): + index = self.get_index() + return [expr for expr in expressions if expr in index] + + def extend(self, expressions, properties): + if properties: + if self.properties is None: + self.properties = [None] * len(self.expressions) + self.properties.extend(properties) + self.expressions.extend(expressions) + self.index = None + return expressions + + def delete(self, expressions): + index = self.get_index() + trash = set(index[x] for x in expressions) + deleted = [self.expressions[i] for i in trash] + self.expressions = [x for i, x in enumerate(self.expressions) if i not in trash] + self.properties = [x for i, x in enumerate(self.properties) if i not in trash] + self.index = None + return deleted + + def data(self): + return self.expressions, list(self.get_properties()) + + def get_index(self): + index = self.index + if index is None: + index = dict((v, i) for i, v in enumerate(self.expressions)) + self.index = index + return index + + def get_properties(self): + if self.properties: + for p in self.properties: + yield p + else: + for _ in range(len(self.expressions)): + yield None + + def get_sorted(self): + index = self.get_index() + return lambda c: sorted(c, key=lambda v: index[v]) + + def get_property(self, element, name): + properties = self.properties + if properties is None: + return None + index = self.get_index() + i = index.get(element) + if i is None: + return None + p = properties[i] + if p is None: + return None + return p.get(name) + + +def _is_connected(G): + if len(G) == 0: # empty graph? + return True + elif G.is_directed(): + return nx.is_strongly_connected(G) + else: + return nx.is_connected(G) + + +def _edge_weights(options): + expr = options.get("System`EdgeWeight") + if expr is None: + return [] + if not expr.has_form("List", None): + return [] + return expr.elements + + +class _GraphParseError(Exception): + pass + + class Graph(Atom): class_head_name = "Pymathics`Graph" @@ -462,8 +541,8 @@ def __hash__(self): def __str__(self): return "-Graph-" - def atom_to_boxes(self, f, evaluation) -> _BoxedString: - return _BoxedString("-Graph-") + def atom_to_boxes(self, f, evaluation) -> _boxed_string: + return _boxed_string("-Graph-") def add_edges(self, new_edges, new_edge_properties): G = self.G.copy() @@ -605,98 +684,6 @@ def vertices(self): return self.G.nodes -class _Collection: - def __init__(self, expressions, properties=None, index=None): - self.expressions = expressions - self.properties = properties if properties else None - self.index = index - - def clone(self): - properties = self.properties - return _Collection( - self.expressions[:], properties[:] if properties else None, None - ) - - def filter(self, expressions): - index = self.get_index() - return [expr for expr in expressions if expr in index] - - def extend(self, expressions, properties): - if properties: - if self.properties is None: - self.properties = [None] * len(self.expressions) - self.properties.extend(properties) - self.expressions.extend(expressions) - self.index = None - return expressions - - def delete(self, expressions): - index = self.get_index() - trash = set(index[x] for x in expressions) - deleted = [self.expressions[i] for i in trash] - self.expressions = [x for i, x in enumerate(self.expressions) if i not in trash] - self.properties = [x for i, x in enumerate(self.properties) if i not in trash] - self.index = None - return deleted - - def data(self): - return self.expressions, list(self.get_properties()) - - def get_index(self): - index = self.index - if index is None: - index = dict((v, i) for i, v in enumerate(self.expressions)) - self.index = index - return index - - def get_properties(self): - if self.properties: - for p in self.properties: - yield p - else: - for _ in range(len(self.expressions)): - yield None - - def get_sorted(self): - index = self.get_index() - return lambda c: sorted(c, key=lambda v: index[v]) - - def get_property(self, element, name): - properties = self.properties - if properties is None: - return None - index = self.get_index() - i = index.get(element) - if i is None: - return None - p = properties[i] - if p is None: - return None - return p.get(name) - - -def _is_connected(G): - if len(G) == 0: # empty graph? - return True - elif G.is_directed(): - return nx.is_strongly_connected(G) - else: - return nx.is_connected(G) - - -def _edge_weights(options): - expr = options.get("System`EdgeWeight") - if expr is None: - return [] - if not expr.has_form("List", None): - return [] - return expr.elements - - -class _GraphParseError(Exception): - pass - - def _parse_item(x, attr_dict=None): if x.has_form("Property", 2): expr, prop = x.elements @@ -892,287 +879,273 @@ def full_new_edge_properties(new_edge_style): return g -class Property(Builtin): +class _Centrality(_NetworkXBuiltin): pass -class PropertyValue(Builtin): - """ - >> g = Graph[{a <-> b, Property[b <-> c, SomeKey -> 123]}]; - >> PropertyValue[{g, b <-> c}, SomeKey] - = 123 - >> PropertyValue[{g, b <-> c}, SomeUnknownKey] - = $Failed - """ +class _ComponentwiseCentrality(_Centrality): + def _centrality(self, g, weight): + raise NotImplementedError - requires = ("networkx",) + def _compute(self, graph, evaluation, reverse=False, normalized=True, **kwargs): + vertices = graph.vertices + G, weight = graph.coalesced_graph(evaluation) + if reverse: + G = G.reverse() - def apply(self, graph, item, name, evaluation): - "PropertyValue[{graph_Graph, item_}, name_Symbol]" - value = graph.get_property(item, name.get_name()) - if value is None: - return SymbolFailed - return value + components = list(_components(G)) + components = [c for c in components if len(c) > 1] + result = [0] * len(vertices) + for bunch in components: + g = G.subgraph(bunch) + centrality = self._centrality(g, weight, **kwargs) + values = [centrality.get(v, 0) for v in vertices] + if normalized: + s = sum(values) * len(components) + else: + s = 1 + if s > 0: + for i, x in enumerate(values): + result[i] += x / s -class DirectedEdge(Builtin): - """ -
-
'DirectedEdge[$u$, $v$]' -
create a directed edge from $u$ to $v$. -
- """ - - summary_text = "make a directed graph edge" - pass + return ListExpression(*[Real(x) for x in result]) -class UndirectedEdge(Builtin): - """ -
-
'UndirectedEdge[$u$, $v$]' -
create an undirected edge between $u$ and $v$. -
+class _PatternList(_NetworkXBuiltin): + def eval(self, graph, expression, evaluation, options): + "%(name)s[graph_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + return ListExpression(*self._items(graph)) - >> a <-> b - = UndirectedEdge[a, b] + def eval_patt(self, graph, patt, expression, evaluation, options): + "%(name)s[graph_, patt_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + return Expression(SymbolCases, ListExpression(*self._items(graph)), patt) - >> (a <-> b) <-> c - = UndirectedEdge[UndirectedEdge[a, b], c] - >> a <-> (b <-> c) - = UndirectedEdge[a, UndirectedEdge[b, c]] +class AcyclicGraphQ(_NetworkXBuiltin): """ + >> g = Graph[{1 -> 2, 2 -> 3}]; AcyclicGraphQ[g] + = True - summary_text = "makes undirected graph edge" + >> g = Graph[{1 -> 2, 2 -> 3, 5 -> 2, 3 -> 4, 3 -> 5}]; AcyclicGraphQ[g] + = False - pass + #> g = Graph[{1 -> 2, 2 -> 3, 5 -> 2, 3 -> 4, 5 -> 3}]; AcyclicGraphQ[g] + = True + + #> g = Graph[{1 -> 2, 2 -> 3, 5 -> 2, 3 -> 4, 5 <-> 3}]; AcyclicGraphQ[g] + = False + #> g = Graph[{1 <-> 2, 2 <-> 3, 5 <-> 2, 3 <-> 4, 5 <-> 3}]; AcyclicGraphQ[g] + = False -class GraphAtom(AtomBuiltin): + #> g = Graph[{}]; AcyclicGraphQ[{}] + = False + + #> AcyclicGraphQ["abc"] + = False + : Expected a graph at position 1 in AcyclicGraphQ[abc]. """ -
-
'Graph[{$e1, $e2, ...}]' -
returns a graph with edges $e_j$. -
-
-
'Graph[{v1, v2, ...}, {$e1, $e2, ...}]' -
returns a graph with vertices $v_i$ and edges $e_j$. -
+ def eval(self, graph, expression, evaluation, options): + "%(name)s[graph_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression, quiet=False) + if not graph or graph.empty(): + return SymbolFalse - >> Graph[{1->2, 2->3, 3->1}] - = -Graph- + try: + cycles = nx.find_cycle(graph.G) + except nx.exception.NetworkXNoCycle: + return SymbolTrue + return from_python(not cycles) - #>> Graph[{1->2, 2->3, 3->1}, EdgeStyle -> {Red, Blue, Green}] - # = -Graph- - >> Graph[{1->2, Property[2->3, EdgeStyle -> Thick], 3->1}] - = -Graph- +class AdjacencyList(_NetworkXBuiltin): + """ + >> AdjacencyList[{1 -> 2, 2 -> 3}, 3] + = {2} - #>> Graph[{1->2, 2->3, 3->1}, VertexStyle -> {1 -> Green, 3 -> Blue}] - #= -Graph- + >> AdjacencyList[{1 -> 2, 2 -> 3}, _?EvenQ] + = {1, 3} - >> Graph[x] - = Graph[x] + >> AdjacencyList[{x -> 2, x -> 3, x -> 4, 2 -> 10, 2 -> 11, 4 -> 20, 4 -> 21, 10 -> 100}, 10, 2] + = {2, 11, 100, x} + """ - >> Graph[{1}] - = Graph[{1}] + def _retrieve(self, graph, what, neighbors, expression, evaluation): + from mathics.builtin import pattern_objects - >> Graph[{{1 -> 2}}] - = Graph[{{1 -> 2}}] + if what.get_head_name() in pattern_objects: + collected = set() + match = Matcher(what).match + for v in graph.G.nodes: + if match(v, evaluation): + collected.update(neighbors(v)) + return ListExpression(*sorted(collected)) + elif graph.G.has_node(what): + return ListExpression(*sorted(neighbors(what))) + else: + self._not_a_vertex(expression, 2, evaluation) - >> g = Graph[{1 -> 2, 2 -> 3}, DirectedEdges -> True]; - >> EdgeCount[g, _DirectedEdge] - = 2 - >> g = Graph[{1 -> 2, 2 -> 3}, DirectedEdges -> False]; - >> EdgeCount[g, _DirectedEdge] - = 0 - >> EdgeCount[g, _UndirectedEdge] - = 2 - """ + def eval(self, graph, what, expression, evaluation, options): + "%(name)s[graph_, what_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + G = graph.G.to_undirected() # FIXME inefficient + return self._retrieve( + graph, what, lambda v: G.neighbors(v), expression, evaluation + ) - requires = ("networkx",) + def eval_d(self, graph, what, d, expression, evaluation, options): + "%(name)s[graph_, what_, d_, OptionsPattern[%(name)s]]" + py_d = d.to_mpmath() + if py_d is None: + return - options = DEFAULT_GRAPH_OPTIONS + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + G = graph.G - def apply(self, graph, evaluation, options): - "Graph[graph_List, OptionsPattern[%(name)s]]" - return _graph_from_list(graph.elements, options) + def neighbors(v): + return nx.ego_graph( + G, v, radius=py_d, undirected=True, center=False + ).nodes() - def apply_1(self, vertices, edges, evaluation, options): - "Graph[vertices_List, edges_List, OptionsPattern[%(name)s]]" - return _graph_from_list( - edges.elements, options=options, new_vertices=vertices.elements - ) + return self._retrieve(graph, what, neighbors, expression, evaluation) -class PathGraphQ(_NetworkXBuiltin): +class BetweennessCentrality(_Centrality): """ - >> PathGraphQ[Graph[{1 -> 2, 2 -> 3}]] - = True - #> PathGraphQ[Graph[{1 -> 2, 2 -> 3, 3 -> 1}]] - = True - #> PathGraphQ[Graph[{1 <-> 2, 2 <-> 3}]] - = True - >> PathGraphQ[Graph[{1 -> 2, 2 <-> 3}]] - = False - >> PathGraphQ[Graph[{1 -> 2, 3 -> 2}]] - = False - >> PathGraphQ[Graph[{1 -> 2, 2 -> 3, 2 -> 4}]] - = False - >> PathGraphQ[Graph[{1 -> 2, 3 -> 2, 2 -> 4}]] - = False + >> g = Graph[{a -> b, b -> c, d -> c, d -> a, e -> c, d -> b}]; BetweennessCentrality[g] + = {0., 1., 0., 0., 0.} - #> PathGraphQ[Graph[{}]] - = False - #> PathGraphQ[Graph[{1 -> 2, 3 -> 4}]] - = False - #> PathGraphQ[Graph[{1 -> 2, 2 -> 1}]] - = True - >> PathGraphQ[Graph[{1 -> 2, 2 -> 3, 2 -> 3}]] - = False - #> PathGraphQ[Graph[{}]] - = False - #> PathGraphQ["abc"] - = False - #> PathGraphQ[{1 -> 2, 2 -> 3}] - = False + >> g = Graph[{a -> b, b -> c, c -> d, d -> e, e -> c, e -> a}]; BetweennessCentrality[g] + = {3., 3., 6., 6., 6.} """ - def apply(self, graph, expression, evaluation, options): - "PathGraphQ[graph_, OptionsPattern[%(name)s]]" - if not isinstance(graph, Graph) or graph.empty(): - return SymbolFalse + def eval(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) + centrality = nx.betweenness_centrality( + graph.G, normalized=False, weight=weight + ) + return ListExpression( + *[Real(centrality.get(v, 0.0)) for v in graph.vertices], + ) - G = graph.G - if G.is_directed(): - connected = nx.is_semiconnected(G) - else: - connected = nx.is_connected(G) +class ClosenessCentrality(_Centrality): + """ + >> g = Graph[{a -> b, b -> c, d -> c, d -> a, e -> c, d -> b}]; ClosenessCentrality[g] + = {0.666667, 1., 0., 1., 1.} - if connected: - is_path = all(d <= 2 for _, d in G.degree(graph.vertices)) - else: - is_path = False + >> g = Graph[{a -> b, b -> c, c -> d, d -> e, e -> c, e -> a}]; ClosenessCentrality[g] + = {0.4, 0.4, 0.4, 0.5, 0.666667} + """ - return from_python(is_path) + def eval(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) + G = graph.G + if G.is_directed(): + 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], + ) -class MixedGraphQ(_NetworkXBuiltin): +class ConnectedGraphQ(_NetworkXBuiltin): """ - >> g = Graph[{1 -> 2, 2 -> 3}]; MixedGraphQ[g] + >> g = Graph[{1 -> 2, 2 -> 3}]; ConnectedGraphQ[g] = False - >> g = Graph[{1 -> 2, 2 <-> 3}]; MixedGraphQ[g] + >> g = Graph[{1 -> 2, 2 -> 3, 3 -> 1}]; ConnectedGraphQ[g] = True - #> g = Graph[{}]; MixedGraphQ[g] - = False + #> g = Graph[{1 -> 2, 2 -> 3, 2 -> 3, 3 -> 1}]; ConnectedGraphQ[g] + = True - #> MixedGraphQ["abc"] + #> g = Graph[{1 -> 2, 2 -> 3}]; ConnectedGraphQ[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 from_python(graph.is_mixed_graph()) - + >> g = Graph[{1 <-> 2, 2 <-> 3}]; ConnectedGraphQ[g] + = True -class MultigraphQ(_NetworkXBuiltin): - """ - >> g = Graph[{1 -> 2, 2 -> 3}]; MultigraphQ[g] + >> g = Graph[{1 <-> 2, 2 <-> 3, 4 <-> 5}]; ConnectedGraphQ[g] = False - >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 2}]; MultigraphQ[g] + #> ConnectedGraphQ[Graph[{}]] = True - #> g = Graph[{}]; MultigraphQ[g] - = False - - #> MultigraphQ["abc"] + #> ConnectedGraphQ["abc"] = False """ - def apply(self, graph, expression, evaluation, options): + def eval(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression, quiet=True) if graph: - return from_python(graph.is_multigraph()) + return from_python(_is_connected(graph.G)) else: return SymbolFalse -class AcyclicGraphQ(_NetworkXBuiltin): +class DegreeCentrality(_Centrality): """ - >> g = Graph[{1 -> 2, 2 -> 3}]; AcyclicGraphQ[g] - = True - - >> g = Graph[{1 -> 2, 2 -> 3, 5 -> 2, 3 -> 4, 3 -> 5}]; AcyclicGraphQ[g] - = False - - #> g = Graph[{1 -> 2, 2 -> 3, 5 -> 2, 3 -> 4, 5 -> 3}]; AcyclicGraphQ[g] - = True - - #> g = Graph[{1 -> 2, 2 -> 3, 5 -> 2, 3 -> 4, 5 <-> 3}]; AcyclicGraphQ[g] - = False - - #> g = Graph[{1 <-> 2, 2 <-> 3, 5 <-> 2, 3 <-> 4, 5 <-> 3}]; AcyclicGraphQ[g] - = False + >> g = Graph[{a -> b, b <-> c, d -> c, d -> a, e <-> c, d -> b}]; DegreeCentrality[g] + = {2, 4, 5, 3, 2} - #> g = Graph[{}]; AcyclicGraphQ[{}] - = False + >> g = Graph[{a -> b, b <-> c, d -> c, d -> a, e <-> c, d -> b}]; DegreeCentrality[g, "In"] + = {1, 3, 3, 0, 1} - #> AcyclicGraphQ["abc"] - = False - : Expected a graph at position 1 in AcyclicGraphQ[abc]. + >> g = Graph[{a -> b, b <-> c, d -> c, d -> a, e <-> c, d -> b}]; DegreeCentrality[g, "Out"] + = {1, 1, 2, 3, 1} """ - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - 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) + 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], + ) + def eval(self, graph, expression, evaluation, options): + "%(name)s[graph_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + return self._from_dict(graph, nx.degree_centrality(graph.G)) -class LoopFreeGraphQ(_NetworkXBuiltin): - """ - >> g = Graph[{1 -> 2, 2 -> 3}]; LoopFreeGraphQ[g] - = True + def eval_in(self, graph, expression, evaluation, options): + '%(name)s[graph_, "In", OptionsPattern[%(name)s]]' + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + return self._from_dict(graph, nx.in_degree_centrality(graph.G)) - >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 1}]; LoopFreeGraphQ[g] - = False + def eval_out(self, graph, expression, evaluation, options): + '%(name)s[graph_, "Out", OptionsPattern[%(name)s]]' + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + return self._from_dict(graph, nx.out_degree_centrality(graph.G)) - #> g = Graph[{}]; LoopFreeGraphQ[{}] - = False - #> LoopFreeGraphQ["abc"] - = False +class DirectedEdge(Builtin): + """ +
+
'DirectedEdge[$u$, $v$]' +
create a directed edge from $u$ to $v$. +
""" - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression, quiet=True) - if not graph or graph.empty(): - return SymbolFalse - - return from_python(graph.is_loop_free()) + summary_text = "make a directed graph edge" + pass class DirectedGraphQ(_NetworkXBuiltin): @@ -1190,7 +1163,7 @@ class DirectedGraphQ(_NetworkXBuiltin): = False """ - def apply(self, graph, expression, evaluation, options): + def eval(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression, quiet=True) if graph: @@ -1200,138 +1173,209 @@ def apply(self, graph, expression, evaluation, options): return SymbolFalse -class ConnectedGraphQ(_NetworkXBuiltin): +class EdgeConnectivity(_NetworkXBuiltin): """ - >> g = Graph[{1 -> 2, 2 -> 3}]; ConnectedGraphQ[g] - = False + >> EdgeConnectivity[{1 <-> 2, 2 <-> 3}] + = 1 - >> g = Graph[{1 -> 2, 2 -> 3, 3 -> 1}]; ConnectedGraphQ[g] - = True + >> EdgeConnectivity[{1 -> 2, 2 -> 3}] + = 0 - #> g = Graph[{1 -> 2, 2 -> 3, 2 -> 3, 3 -> 1}]; ConnectedGraphQ[g] - = True + >> EdgeConnectivity[{1 -> 2, 2 -> 3, 3 -> 1}] + = 1 - #> g = Graph[{1 -> 2, 2 -> 3}]; ConnectedGraphQ[g] - = False + >> EdgeConnectivity[{1 <-> 2, 2 <-> 3, 1 <-> 3}] + = 2 - >> g = Graph[{1 <-> 2, 2 <-> 3}]; ConnectedGraphQ[g] - = True + >> EdgeConnectivity[{1 <-> 2, 3 <-> 4}] + = 0 - >> g = Graph[{1 <-> 2, 2 <-> 3, 4 <-> 5}]; ConnectedGraphQ[g] - = False + #> EdgeConnectivity[Graph[{}]] + = EdgeConnectivity[-Graph-] + """ - #> ConnectedGraphQ[Graph[{}]] - = True + def eval(self, graph, expression, evaluation, options): + "%(name)s[graph_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph and not graph.empty(): + return Integer(nx.edge_connectivity(graph.G)) - #> ConnectedGraphQ["abc"] - = False + def eval_st(self, graph, s, t, expression, evaluation, options): + "%(name)s[graph_, s_, t_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph and not graph.empty(): + return Integer(nx.edge_connectivity(graph.G, s, t)) + + +class EdgeIndex(_NetworkXBuiltin): + """ + >> EdgeIndex[{c <-> d, d <-> a, a -> e}, d <-> a] + = 2 """ - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression, quiet=True) + def eval(self, graph, v, expression, evaluation, options): + "%(name)s[graph_, v_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) if graph: - return from_python(_is_connected(graph.G)) - else: - return SymbolFalse + i = graph.edges.get_index().get(v) + if i is None: + self._not_an_edge(expression, 2, evaluation) + else: + return Integer(i + 1) -class SimpleGraphQ(_NetworkXBuiltin): +class EdgeList(_PatternList): + """ + >> EdgeList[{1 -> 2, 2 <-> 3}] + = {DirectedEdge[1, 2], UndirectedEdge[2, 3]} """ - >> g = Graph[{1 -> 2, 2 -> 3, 3 <-> 4}]; SimpleGraphQ[g] - = True - - >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 1}]; SimpleGraphQ[g] - = False - >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 2}]; SimpleGraphQ[g] - = False + def _items(self, graph): + return graph.edges - #> SimpleGraphQ[Graph[{}]] - = True - #> SimpleGraphQ["abc"] - = False +class EdgeRules(_NetworkXBuiltin): + """ + >> EdgeRules[{1 <-> 2, 2 -> 3, 3 <-> 4}] + = {1 -> 2, 2 -> 3, 3 -> 4} """ - def apply(self, graph, expression, evaluation, options): + def eval(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression, quiet=True) + graph = self._build_graph(graph, evaluation, options, expression) if graph: - if graph.empty(): - return SymbolTrue - else: - simple = graph.is_loop_free() and not graph.is_multigraph() - return from_python(simple) - else: - return SymbolFalse + + def rules(): + for expr in graph.edges.expressions: + u, v = expr.elements + yield Expression(SymbolRule, u, v) + + return ListExpression(*list(rules())) -class PlanarGraphQ(_NetworkXBuiltin): +class EigenvectorCentrality(_ComponentwiseCentrality): """ - See https://en.wikipedia.org/wiki/Planar_graph + >> g = Graph[{a -> b, b -> c, c -> d, d -> e, e -> c, e -> a}]; EigenvectorCentrality[g, "In"] + = {0.16238, 0.136013, 0.276307, 0.23144, 0.193859} - >> PlanarGraphQ[CompleteGraph[4]] - = True + >> EigenvectorCentrality[g, "Out"] + = {0.136013, 0.16238, 0.193859, 0.23144, 0.276307} - >> PlanarGraphQ[CompleteGraph[5]] - = False + >> g = Graph[{a <-> b, b <-> c, c <-> d, d <-> e, e <-> c, e <-> a}]; EigenvectorCentrality[g] + = {0.162435, 0.162435, 0.240597, 0.193937, 0.240597} - #> PlanarGraphQ[Graph[{}]] - = False + >> g = Graph[{a <-> b, b <-> c, a <-> c, d <-> e, e <-> f, f <-> d, e <-> d}]; EigenvectorCentrality[g] + = {0.166667, 0.166667, 0.166667, 0.183013, 0.183013, 0.133975} - #> PlanarGraphQ["abc"] - = False + #> g = Graph[{a -> b, b -> c, c -> d, b -> e, a -> e}]; EigenvectorCentrality[g] + = {0., 0., 0., 0., 0.} + + >> g = Graph[{a -> b, b -> c, c -> d, b -> e, a -> e, c -> a}]; EigenvectorCentrality[g] + = {0.333333, 0.333333, 0.333333, 0., 0.} """ - requires = _NetworkXBuiltin.requires + def _centrality(self, g, weight): + return nx.eigenvector_centrality(g, max_iter=10000, tol=1.0e-7, weight=weight) - def apply(self, graph, expression, evaluation, options): + def eval(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression, quiet=True) - if not graph or graph.empty(): - return SymbolFalse + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + return self._compute(graph, evaluation) - return from_python(nx.is_planar(graph.G)) + def eval_in_out(self, graph, dir, expression, evaluation, options): + "%(name)s[graph_, dir_String, OptionsPattern[%(name)s]]" + py_dir = dir.get_string_value() + if py_dir not in ("In", "Out"): + return + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + return self._compute(graph, evaluation, py_dir == "Out") -class FindVertexCut(_NetworkXBuiltin): +class FindShortestPath(_NetworkXBuiltin): """ -
-
'FindVertexCut[$g$]' -
finds a set of vertices of minimum cardinality that, if removed, renders $g$ disconnected. -
'FindVertexCut[$g$, $s$, $t$]' -
finds a vertex cut that disconnects all paths from $s$ to $t$. -
+ >> FindShortestPath[{1 <-> 2, 2 <-> 3, 3 <-> 4, 2 <-> 4, 4 -> 5}, 1, 5] + = {1, 2, 4, 5} - >> g = Graph[{1 -> 2, 2 -> 3}]; FindVertexCut[g] + >> FindShortestPath[{1 <-> 2, 2 <-> 3, 3 <-> 4, 4 -> 2, 4 -> 5}, 1, 5] + = {1, 2, 3, 4, 5} + + >> FindShortestPath[{1 <-> 2, 2 <-> 3, 4 -> 3, 4 -> 2, 4 -> 5}, 1, 5] = {} - >> g = Graph[{1 <-> 2, 2 <-> 3}]; FindVertexCut[g] - = {2} + >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 3}, EdgeWeight -> {0.5, a, 3}]; + >> a = 0.5; FindShortestPath[g, 1, 3] + = {1, 2, 3} + >> a = 10; FindShortestPath[g, 1, 3] + = {1, 3} - >> g = Graph[{1 <-> x, x <-> 2, 1 <-> y, y <-> 2, x <-> y}]; FindVertexCut[g] - = {x, y} + #> FindShortestPath[{}, 1, 2] + : The vertex at position 2 in FindShortestPath[{}, 1, 2] does not belong to the graph at position 1. + = FindShortestPath[{}, 1, 2] - #> FindVertexCut[Graph[{}]] - = {} - #> FindVertexCut[Graph[{}], 1, 2] - : The vertex at position 2 in FindVertexCut[-Graph-, 1, 2] does not belong to the graph at position 1. - = FindVertexCut[-Graph-, 1, 2] + #> FindShortestPath[{1 -> 2}, 1, 3] + : The vertex at position 3 in FindShortestPath[{1 -> 2}, 1, 3] does not belong to the graph at position 1. + = FindShortestPath[{1 -> 2}, 1, 3] """ - def apply(self, graph, expression, evaluation, options): - "FindVertexCut[graph_, OptionsPattern[%(name)s]]" + def eval_s_t(self, graph, s, t, expression, evaluation, options): + "%(name)s[graph_, s_, t_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression) - if graph: - if graph.empty() or not _is_connected(graph.G): - return ListExpression() - else: - return ListExpression( - *graph.sort_vertices(nx.minimum_node_cut(graph.G)) - ) + if not graph: + return + G = graph.G + if not G.has_node(s): + self._not_a_vertex(expression, 2, evaluation) + elif not G.has_node(t): + self._not_a_vertex(expression, 3, evaluation) + else: + try: + weight = graph.update_weights(evaluation) + return ListExpression( + *list(nx.shortest_path(G, source=s, target=t, weight=weight)), + ) + except nx.exception.NetworkXNoPath: + return ListExpression() + + +class FindVertexCut(_NetworkXBuiltin): + """ +
+
'FindVertexCut[$g$]' +
finds a set of vertices of minimum cardinality that, if removed, renders $g$ disconnected. +
'FindVertexCut[$g$, $s$, $t$]' +
finds a vertex cut that disconnects all paths from $s$ to $t$. +
+ + >> g = Graph[{1 -> 2, 2 -> 3}]; FindVertexCut[g] + = {} + + >> g = Graph[{1 <-> 2, 2 <-> 3}]; FindVertexCut[g] + = {2} + + >> g = Graph[{1 <-> x, x <-> 2, 1 <-> y, y <-> 2, x <-> y}]; FindVertexCut[g] + = {x, y} + + #> FindVertexCut[Graph[{}]] + = {} + #> FindVertexCut[Graph[{}], 1, 2] + : The vertex at position 2 in FindVertexCut[-Graph-, 1, 2] does not belong to the graph at position 1. + = FindVertexCut[-Graph-, 1, 2] + """ + + def eval(self, graph, expression, evaluation, options): + "FindVertexCut[graph_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + if graph.empty() or not _is_connected(graph.G): + return ListExpression() + else: + return ListExpression( + *graph.sort_vertices(nx.minimum_node_cut(graph.G)) + ) - def apply_st(self, graph, s, t, expression, evaluation, options): + def eval_st(self, graph, s, t, expression, evaluation, options): "FindVertexCut[graph_, s_, t_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression) if not graph: @@ -1348,10 +1392,115 @@ def apply_st(self, graph, s, t, expression, evaluation, options): return ListExpression(*graph.sort_vertices(nx.minimum_node_cut(G, s, t))) +class GraphAtom(AtomBuiltin): + """ +
+
'Graph[{$e1, $e2, ...}]' +
returns a graph with edges $e_j$. +
+ +
+
'Graph[{v1, v2, ...}, {$e1, $e2, ...}]' +
returns a graph with vertices $v_i$ and edges $e_j$. +
+ + >> Graph[{1->2, 2->3, 3->1}] + = -Graph- + + #>> Graph[{1->2, 2->3, 3->1}, EdgeStyle -> {Red, Blue, Green}] + # = -Graph- + + >> Graph[{1->2, Property[2->3, EdgeStyle -> Thick], 3->1}] + = -Graph- + + #>> Graph[{1->2, 2->3, 3->1}, VertexStyle -> {1 -> Green, 3 -> Blue}] + #= -Graph- + + >> Graph[x] + = Graph[x] + + >> Graph[{1}] + = Graph[{1}] + + >> Graph[{{1 -> 2}}] + = Graph[{{1 -> 2}}] + + >> g = Graph[{1 -> 2, 2 -> 3}, DirectedEdges -> True]; + >> EdgeCount[g, _DirectedEdge] + = 2 + >> g = Graph[{1 -> 2, 2 -> 3}, DirectedEdges -> False]; + >> EdgeCount[g, _DirectedEdge] + = 0 + >> EdgeCount[g, _UndirectedEdge] + = 2 + """ + + requires = ("networkx",) + + options = DEFAULT_GRAPH_OPTIONS + + def eval(self, graph, evaluation, options): + "Graph[graph_List, OptionsPattern[%(name)s]]" + return _graph_from_list(graph.elements, options) + + def eval_1(self, vertices, edges, evaluation, options): + "Graph[vertices_List, edges_List, OptionsPattern[%(name)s]]" + return _graph_from_list( + edges.elements, options=options, new_vertices=vertices.elements + ) + + +class GraphBox(GraphicsBox): + def _graphics_box(self, elements, options): + evaluation = options["evaluation"] + graph, form = elements + primitives = graph._layout(evaluation) + graphics = Expression(SymbolGraphics, primitives) + graphics_box = Expression(SymbolMakeBoxes, graphics, form).evaluate(evaluation) + return graphics_box + + def boxes_to_text(self, elements, **options): + return "-Graph-" + + def boxes_to_xml(self, elements, **options): + # Figure out what to do here. + return "-Graph-XML-" + + def boxes_to_tex(self, elements, **options): + # Figure out what to do here. + return "-Graph-TeX-" + + +class HITSCentrality(_Centrality): + """ + >> g = Graph[{a -> d, b -> c, d -> c, d -> a, e -> c}]; HITSCentrality[g] + = {{0.292893, 0., 0., 0.707107, 0.}, {0., 1., 0.707107, 0., 0.707107}} + """ + + def eval(self, graph, expression, evaluation, options): + "%(name)s[graph_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + G, _ = graph.coalesced_graph(evaluation) # FIXME warn if weight > 1 + + tol = 1.0e-14 + _, a = nx.hits(G, normalized=True, tol=tol) + h, _ = nx.hits(G, normalized=False, tol=tol) + + def _crop(x): + return 0 if x < tol else x + + 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]), + ) + + class HighlightGraph(_NetworkXBuiltin): """""" - def apply(self, graph, what, expression, evaluation, options): + def eval(self, graph, what, expression, evaluation, options): "HighlightGraph[graph_, what_List, OptionsPattern[%(name)s]]" default_highlight = [Expression(SymbolRGBColor, Integer1, Integer0, Integer0)] @@ -1377,569 +1526,267 @@ def parse(item): return graph.with_highlight(rule_exprs) -class _PatternList(_NetworkXBuiltin): - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - return ListExpression(*self._items(graph)) +class KatzCentrality(_ComponentwiseCentrality): + """ + >> g = Graph[{a -> b, b -> c, c -> d, d -> e, e -> c, e -> a}]; KatzCentrality[g, 0.2] + = {1.25202, 1.2504, 1.5021, 1.30042, 1.26008} - def apply_patt(self, graph, patt, expression, evaluation, options): - "%(name)s[graph_, patt_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - return Expression(SymbolCases, ListExpression(*self._items(graph)), patt) + >> g = Graph[{a <-> b, b <-> c, a <-> c, d <-> e, e <-> f, f <-> d, e <-> d}]; KatzCentrality[g, 0.1] + = {1.25, 1.25, 1.25, 1.41026, 1.41026, 1.28205} + """ + rules = { + "KatzCentrality[g_, alpha_]": "KatzCentrality[g, alpha, 1]", + } -class _PatternCount(_NetworkXBuiltin): - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - return Integer(len(self._items(graph))) + def _centrality(self, g, weight, alpha, beta): + return nx.katz_centrality( + g, alpha=alpha, beta=beta, normalized=False, weight=weight + ) - def apply_patt(self, graph, patt, expression, evaluation, options): - "%(name)s[graph_, patt_, OptionsPattern[%(name)s]]" + def eval(self, graph, alpha, beta, expression, evaluation, options): + "%(name)s[graph_, alpha_, beta_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression) if graph: - return Expression( - SymbolLength, - Expression(SymbolCases, ListExpression(*self._items(graph)), patt), + py_alpha = alpha.to_mpmath() + py_beta = beta.to_mpmath() + if py_alpha is None or py_beta is None: + return + return self._compute( + graph, evaluation, normalized=False, alpha=py_alpha, beta=py_beta ) -class VertexCount(_PatternCount): - """ - >> VertexCount[{1 -> 2, 2 -> 3}] - = 3 - - >> VertexCount[{1 -> x, x -> 3}, _Integer] - = 2 +class LoopFreeGraphQ(_NetworkXBuiltin): """ + >> g = Graph[{1 -> 2, 2 -> 3}]; LoopFreeGraphQ[g] + = True - def _items(self, graph): - return graph.vertices.expressions + >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 1}]; LoopFreeGraphQ[g] + = False + #> g = Graph[{}]; LoopFreeGraphQ[{}] + = False -class VertexList(_PatternList): + #> LoopFreeGraphQ["abc"] + = False """ - >> VertexList[{1 -> 2, 2 -> 3}] - = {1, 2, 3} - >> VertexList[{a -> c, c -> b}] - = {a, c, b} - - >> VertexList[{a -> c, 5 -> b}, _Integer -> 10] - = {10} - """ + def eval(self, graph, expression, evaluation, options): + "%(name)s[graph_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression, quiet=True) + if not graph or graph.empty(): + return SymbolFalse - def _items(self, graph): - return graph.vertices + return from_python(graph.is_loop_free()) -class EdgeCount(_PatternCount): - """ - >> EdgeCount[{1 -> 2, 2 -> 3}] - = 2 +class MixedGraphQ(_NetworkXBuiltin): """ + >> g = Graph[{1 -> 2, 2 -> 3}]; MixedGraphQ[g] + = False - def _items(self, graph): - return graph.G.edges - - -class EdgeList(_PatternList): - """ - >> EdgeList[{1 -> 2, 2 <-> 3}] - = {DirectedEdge[1, 2], UndirectedEdge[2, 3]} - """ + >> g = Graph[{1 -> 2, 2 <-> 3}]; MixedGraphQ[g] + = True - def _items(self, graph): - return graph.edges + #> g = Graph[{}]; MixedGraphQ[g] + = False + #> MixedGraphQ["abc"] + = False -class EdgeRules(_NetworkXBuiltin): - """ - >> EdgeRules[{1 <-> 2, 2 -> 3, 3 <-> 4}] - = {1 -> 2, 2 -> 3, 3 -> 4} + # #> 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): + def eval(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) + graph = self._build_graph(graph, evaluation, options, expression, quiet=True) if graph: + return from_python(graph.is_mixed_graph()) - def rules(): - for expr in graph.edges.expressions: - u, v = expr.elements - yield Expression(SymbolRule, u, v) - return ListExpression(*list(rules())) - - -class AdjacencyList(_NetworkXBuiltin): - """ - >> AdjacencyList[{1 -> 2, 2 -> 3}, 3] - = {2} - - >> AdjacencyList[{1 -> 2, 2 -> 3}, _?EvenQ] - = {1, 3} - - >> AdjacencyList[{x -> 2, x -> 3, x -> 4, 2 -> 10, 2 -> 11, 4 -> 20, 4 -> 21, 10 -> 100}, 10, 2] - = {2, 11, 100, x} - """ - - def _retrieve(self, graph, what, neighbors, expression, evaluation): - from mathics.builtin import pattern_objects - - if what.get_head_name() in pattern_objects: - collected = set() - match = Matcher(what).match - for v in graph.G.nodes: - if match(v, evaluation): - collected.update(neighbors(v)) - return ListExpression(*sorted(collected)) - elif graph.G.has_node(what): - return ListExpression(*sorted(neighbors(what))) - else: - self._not_a_vertex(expression, 2, evaluation) - - 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: - G = graph.G.to_undirected() # FIXME inefficient - return self._retrieve( - graph, what, lambda v: G.neighbors(v), expression, evaluation - ) - - def apply_d(self, graph, what, d, expression, evaluation, options): - "%(name)s[graph_, what_, d_, OptionsPattern[%(name)s]]" - py_d = d.to_mpmath() - if py_d is None: - return - - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - G = graph.G - - def neighbors(v): - return nx.ego_graph( - G, v, radius=py_d, undirected=True, center=False - ).nodes() - - return self._retrieve(graph, what, neighbors, expression, evaluation) - - -class VertexIndex(_NetworkXBuiltin): - """ - >> VertexIndex[{c <-> d, d <-> a}, a] - = 3 - """ - - def apply(self, graph, v, expression, evaluation, options): - "%(name)s[graph_, v_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - i = graph.vertices.get_index().get(v) - if i is None: - self._not_a_vertex(expression, 2, evaluation) - else: - return Integer(i + 1) - - -class EdgeIndex(_NetworkXBuiltin): - """ - >> EdgeIndex[{c <-> d, d <-> a, a -> e}, d <-> a] - = 2 - """ - - def apply(self, graph, v, expression, evaluation, options): - "%(name)s[graph_, v_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - i = graph.edges.get_index().get(v) - if i is None: - self._not_an_edge(expression, 2, evaluation) - else: - return Integer(i + 1) - - -class EdgeConnectivity(_NetworkXBuiltin): - """ - >> EdgeConnectivity[{1 <-> 2, 2 <-> 3}] - = 1 - - >> EdgeConnectivity[{1 -> 2, 2 -> 3}] - = 0 - - >> EdgeConnectivity[{1 -> 2, 2 -> 3, 3 -> 1}] - = 1 - - >> EdgeConnectivity[{1 <-> 2, 2 <-> 3, 1 <-> 3}] - = 2 - - >> EdgeConnectivity[{1 <-> 2, 3 <-> 4}] - = 0 - - #> EdgeConnectivity[Graph[{}]] - = EdgeConnectivity[-Graph-] - """ - - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph and not graph.empty(): - return Integer(nx.edge_connectivity(graph.G)) - - def apply_st(self, graph, s, t, expression, evaluation, options): - "%(name)s[graph_, s_, t_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph and not graph.empty(): - return Integer(nx.edge_connectivity(graph.G, s, t)) - - -class VertexConnectivity(_NetworkXBuiltin): - """ - >> VertexConnectivity[{1 <-> 2, 2 <-> 3}] - = 1 - - >> VertexConnectivity[{1 -> 2, 2 -> 3}] - = 0 - - >> VertexConnectivity[{1 -> 2, 2 -> 3, 3 -> 1}] - = 1 - - >> VertexConnectivity[{1 <-> 2, 2 <-> 3, 1 <-> 3}] - = 2 - - >> VertexConnectivity[{1 <-> 2, 3 <-> 4}] - = 0 - - #> VertexConnectivity[Graph[{}]] - = VertexConnectivity[-Graph-] +class MultigraphQ(_NetworkXBuiltin): """ + >> g = Graph[{1 -> 2, 2 -> 3}]; MultigraphQ[g] + = False - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph and not graph.empty(): - if not _is_connected(graph.G): - return Integer(0) - else: - return Integer(nx.node_connectivity(graph.G)) - - def apply_st(self, graph, s, t, expression, evaluation, options): - "%(name)s[graph_, s_, t_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph and not graph.empty(): - if not _is_connected(graph.G): - return Integer(0) - else: - return Integer(nx.node_connectivity(graph.G, s, t)) - - -class _Centrality(_NetworkXBuiltin): - pass - + >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 2}]; MultigraphQ[g] + = True -class BetweennessCentrality(_Centrality): - """ - >> g = Graph[{a -> b, b -> c, d -> c, d -> a, e -> c, d -> b}]; BetweennessCentrality[g] - = {0., 1., 0., 0., 0.} + #> g = Graph[{}]; MultigraphQ[g] + = False - >> g = Graph[{a -> b, b -> c, c -> d, d -> e, e -> c, e -> a}]; BetweennessCentrality[g] - = {3., 3., 6., 6., 6.} + #> MultigraphQ["abc"] + = False """ - def apply(self, graph, expression, evaluation, options): + def eval(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) + graph = self._build_graph(graph, evaluation, options, expression, quiet=True) if graph: - weight = graph.update_weights(evaluation) - centrality = nx.betweenness_centrality( - graph.G, normalized=False, weight=weight - ) - return ListExpression( - *[Real(centrality.get(v, 0.0)) for v in graph.vertices], - ) + return from_python(graph.is_multigraph()) + else: + return SymbolFalse -class ClosenessCentrality(_Centrality): +class PageRankCentrality(_Centrality): """ - >> g = Graph[{a -> b, b -> c, d -> c, d -> a, e -> c, d -> b}]; ClosenessCentrality[g] - = {0.666667, 1., 0., 1., 1.} - - >> g = Graph[{a -> b, b -> c, c -> d, d -> e, e -> c, e -> a}]; ClosenessCentrality[g] - = {0.4, 0.4, 0.4, 0.5, 0.666667} + >> g = Graph[{a -> d, b -> c, d -> c, d -> a, e -> c, d -> c}]; PageRankCentrality[g, 0.2] + = {0.184502, 0.207565, 0.170664, 0.266605, 0.170664} """ - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" + def eval_alpha_beta(self, graph, alpha, expression, evaluation, options): + "%(name)s[graph_, alpha_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression) if graph: - weight = graph.update_weights(evaluation) - G = graph.G - if G.is_directed(): - G = G.reverse() - centrality = nx.closeness_centrality(G, distance=weight, wf_improved=False) + py_alpha = alpha.to_mpmath() + if py_alpha is None: + return + 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.0)) for v in graph.vertices], + *[Real(centrality.get(v, 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}]; DegreeCentrality[g, "In"] - = {1, 3, 3, 0, 1} - - >> g = Graph[{a -> b, b <-> c, d -> c, d -> a, e <-> c, d -> b}]; DegreeCentrality[g, "Out"] - = {1, 1, 2, 3, 1} - """ - - 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], - ) - - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - return self._from_dict(graph, nx.degree_centrality(graph.G)) - - def apply_in(self, graph, expression, evaluation, options): - '%(name)s[graph_, "In", OptionsPattern[%(name)s]]' - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - return self._from_dict(graph, nx.in_degree_centrality(graph.G)) - - def apply_out(self, graph, expression, evaluation, options): - '%(name)s[graph_, "Out", OptionsPattern[%(name)s]]' - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - return self._from_dict(graph, nx.out_degree_centrality(graph.G)) - - -class _ComponentwiseCentrality(_Centrality): - def _centrality(self, g, weight): - raise NotImplementedError - - def _compute(self, graph, evaluation, reverse=False, normalized=True, **kwargs): - vertices = graph.vertices - G, weight = graph.coalesced_graph(evaluation) - if reverse: - G = G.reverse() - - components = list(_components(G)) - components = [c for c in components if len(c) > 1] - - result = [0] * len(vertices) - for bunch in components: - g = G.subgraph(bunch) - centrality = self._centrality(g, weight, **kwargs) - values = [centrality.get(v, 0) for v in vertices] - if normalized: - s = sum(values) * len(components) - else: - s = 1 - if s > 0: - for i, x in enumerate(values): - result[i] += x / s - - return ListExpression(*[Real(x) for x in result]) - - -class EigenvectorCentrality(_ComponentwiseCentrality): - """ - >> g = Graph[{a -> b, b -> c, c -> d, d -> e, e -> c, e -> a}]; EigenvectorCentrality[g, "In"] - = {0.16238, 0.136013, 0.276307, 0.23144, 0.193859} - - >> EigenvectorCentrality[g, "Out"] - = {0.136013, 0.16238, 0.193859, 0.23144, 0.276307} - - >> g = Graph[{a <-> b, b <-> c, c <-> d, d <-> e, e <-> c, e <-> a}]; EigenvectorCentrality[g] - = {0.162435, 0.162435, 0.240597, 0.193937, 0.240597} - - >> g = Graph[{a <-> b, b <-> c, a <-> c, d <-> e, e <-> f, f <-> d, e <-> d}]; EigenvectorCentrality[g] - = {0.166667, 0.166667, 0.166667, 0.183013, 0.183013, 0.133975} - - #> g = Graph[{a -> b, b -> c, c -> d, b -> e, a -> e}]; EigenvectorCentrality[g] - = {0., 0., 0., 0., 0.} - - >> g = Graph[{a -> b, b -> c, c -> d, b -> e, a -> e, c -> a}]; EigenvectorCentrality[g] - = {0.333333, 0.333333, 0.333333, 0., 0.} - """ - - def _centrality(self, g, weight): - return nx.eigenvector_centrality(g, max_iter=10000, tol=1.0e-7, weight=weight) - - def apply(self, graph, expression, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - return self._compute(graph, evaluation) - - def apply_in_out(self, graph, dir, expression, evaluation, options): - "%(name)s[graph_, dir_String, OptionsPattern[%(name)s]]" - py_dir = dir.get_string_value() - if py_dir not in ("In", "Out"): - return - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - return self._compute(graph, evaluation, py_dir == "Out") - - -class KatzCentrality(_ComponentwiseCentrality): +class PathGraphQ(_NetworkXBuiltin): """ - >> g = Graph[{a -> b, b -> c, c -> d, d -> e, e -> c, e -> a}]; KatzCentrality[g, 0.2] - = {1.25202, 1.2504, 1.5021, 1.30042, 1.26008} + >> PathGraphQ[Graph[{1 -> 2, 2 -> 3}]] + = True + #> PathGraphQ[Graph[{1 -> 2, 2 -> 3, 3 -> 1}]] + = True + #> PathGraphQ[Graph[{1 <-> 2, 2 <-> 3}]] + = True + >> PathGraphQ[Graph[{1 -> 2, 2 <-> 3}]] + = False + >> PathGraphQ[Graph[{1 -> 2, 3 -> 2}]] + = False + >> PathGraphQ[Graph[{1 -> 2, 2 -> 3, 2 -> 4}]] + = False + >> PathGraphQ[Graph[{1 -> 2, 3 -> 2, 2 -> 4}]] + = False - >> g = Graph[{a <-> b, b <-> c, a <-> c, d <-> e, e <-> f, f <-> d, e <-> d}]; KatzCentrality[g, 0.1] - = {1.25, 1.25, 1.25, 1.41026, 1.41026, 1.28205} + #> PathGraphQ[Graph[{}]] + = False + #> PathGraphQ[Graph[{1 -> 2, 3 -> 4}]] + = False + #> PathGraphQ[Graph[{1 -> 2, 2 -> 1}]] + = True + >> PathGraphQ[Graph[{1 -> 2, 2 -> 3, 2 -> 3}]] + = False + #> PathGraphQ[Graph[{}]] + = False + #> PathGraphQ["abc"] + = False + #> PathGraphQ[{1 -> 2, 2 -> 3}] + = False """ - rules = { - "KatzCentrality[g_, alpha_]": "KatzCentrality[g, alpha, 1]", - } + def eval(self, graph, expression, evaluation, options): + "PathGraphQ[graph_, OptionsPattern[%(name)s]]" + if not isinstance(graph, Graph) or graph.empty(): + return SymbolFalse - def _centrality(self, g, weight, alpha, beta): - return nx.katz_centrality( - g, alpha=alpha, beta=beta, normalized=False, weight=weight - ) + G = graph.G - def apply(self, graph, alpha, beta, expression, evaluation, options): - "%(name)s[graph_, alpha_, beta_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - py_alpha = alpha.to_mpmath() - py_beta = beta.to_mpmath() - if py_alpha is None or py_beta is None: - return - return self._compute( - graph, evaluation, normalized=False, alpha=py_alpha, beta=py_beta - ) + 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 -class PageRankCentrality(_Centrality): - """ - >> g = Graph[{a -> d, b -> c, d -> c, d -> a, e -> c, d -> c}]; PageRankCentrality[g, 0.2] - = {0.184502, 0.207565, 0.170664, 0.266605, 0.170664} + return from_python(is_path) + + +class PlanarGraphQ(_NetworkXBuiltin): """ + See https://en.wikipedia.org/wiki/Planar_graph - def apply_alpha_beta(self, graph, alpha, expression, evaluation, options): - "%(name)s[graph_, alpha_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - py_alpha = alpha.to_mpmath() - if py_alpha is None: - return - 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], - ) + >> PlanarGraphQ[CompleteGraph[4]] + = True + >> PlanarGraphQ[CompleteGraph[5]] + = False -class HITSCentrality(_Centrality): - """ - >> g = Graph[{a -> d, b -> c, d -> c, d -> a, e -> c}]; HITSCentrality[g] - = {{0.292893, 0., 0., 0.707107, 0.}, {0., 1., 0.707107, 0., 0.707107}} + #> PlanarGraphQ[Graph[{}]] + = False + + #> PlanarGraphQ["abc"] + = False """ - def apply(self, graph, expression, evaluation, options): + requires = _NetworkXBuiltin.requires + + def eval(self, graph, expression, evaluation, options): "%(name)s[graph_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if graph: - G, _ = graph.coalesced_graph(evaluation) # FIXME warn if weight > 1 + graph = self._build_graph(graph, evaluation, options, expression, quiet=True) + if not graph or graph.empty(): + return SymbolFalse - tol = 1.0e-14 - _, a = nx.hits(G, normalized=True, tol=tol) - h, _ = nx.hits(G, normalized=False, tol=tol) + return from_python(nx.is_planar(graph.G)) - def _crop(x): - return 0 if x < tol else x - 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]), - ) +class Property(Builtin): + pass -class VertexDegree(_Centrality): +class PropertyValue(Builtin): """ - >> VertexDegree[{1 <-> 2, 2 <-> 3, 2 <-> 4}] - = {1, 3, 1, 1} + >> g = Graph[{a <-> b, Property[b <-> c, SomeKey -> 123]}]; + >> PropertyValue[{g, b <-> c}, SomeKey] + = 123 + >> PropertyValue[{g, b <-> c}, SomeUnknownKey] + = $Failed """ - def apply(self, graph, evaluation, options): - "%(name)s[graph_, OptionsPattern[%(name)s]]" - - def degrees(graph): - degrees = dict(list(graph.G.degree(graph.vertices))) - return ListExpression(*[Integer(degrees.get(v, 0)) for v in graph.vertices]) + requires = ("networkx",) - return self._evaluate_atom(graph, options, degrees) + def eval(self, graph, item, name, evaluation): + "PropertyValue[{graph_Graph, item_}, name_Symbol]" + value = graph.get_property(item, name.get_name()) + if value is None: + return SymbolFailed + return value -class FindShortestPath(_NetworkXBuiltin): +class SimpleGraphQ(_NetworkXBuiltin): """ - >> FindShortestPath[{1 <-> 2, 2 <-> 3, 3 <-> 4, 2 <-> 4, 4 -> 5}, 1, 5] - = {1, 2, 4, 5} - - >> FindShortestPath[{1 <-> 2, 2 <-> 3, 3 <-> 4, 4 -> 2, 4 -> 5}, 1, 5] - = {1, 2, 3, 4, 5} + >> g = Graph[{1 -> 2, 2 -> 3, 3 <-> 4}]; SimpleGraphQ[g] + = True - >> FindShortestPath[{1 <-> 2, 2 <-> 3, 4 -> 3, 4 -> 2, 4 -> 5}, 1, 5] - = {} + >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 1}]; SimpleGraphQ[g] + = False - >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 3}, EdgeWeight -> {0.5, a, 3}]; - >> a = 0.5; FindShortestPath[g, 1, 3] - = {1, 2, 3} - >> a = 10; FindShortestPath[g, 1, 3] - = {1, 3} + >> g = Graph[{1 -> 2, 2 -> 3, 1 -> 2}]; SimpleGraphQ[g] + = False - #> FindShortestPath[{}, 1, 2] - : The vertex at position 2 in FindShortestPath[{}, 1, 2] does not belong to the graph at position 1. - = FindShortestPath[{}, 1, 2] + #> SimpleGraphQ[Graph[{}]] + = True - #> FindShortestPath[{1 -> 2}, 1, 3] - : The vertex at position 3 in FindShortestPath[{1 -> 2}, 1, 3] does not belong to the graph at position 1. - = FindShortestPath[{1 -> 2}, 1, 3] + #> SimpleGraphQ["abc"] + = False """ - def apply_s_t(self, graph, s, t, expression, evaluation, options): - "%(name)s[graph_, s_, t_, OptionsPattern[%(name)s]]" - graph = self._build_graph(graph, evaluation, options, expression) - if not graph: - return - G = graph.G - if not G.has_node(s): - self._not_a_vertex(expression, 2, evaluation) - elif not G.has_node(t): - self._not_a_vertex(expression, 3, evaluation) + def eval(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 SymbolTrue + else: + simple = graph.is_loop_free() and not graph.is_multigraph() + return from_python(simple) else: - try: - weight = graph.update_weights(evaluation) - return ListExpression( - *list(nx.shortest_path(G, source=s, target=t, weight=weight)), - ) - except nx.exception.NetworkXNoPath: - return ListExpression() - - -def _convert_networkx_graph(G, options): - mapping = dict((v, Integer(i)) for i, v in enumerate(G.nodes)) - G = nx.relabel_nodes(G, mapping) - [Expression(SymbolUndirectedEdge, u, v) for u, v in G.edges] - return Graph( - G, - **options, - ) + return SymbolFalse class VertexAdd(_NetworkXBuiltin): @@ -1953,7 +1800,7 @@ class VertexAdd(_NetworkXBuiltin): = -Graph- """ - def apply(self, graph: Expression, what, expression, evaluation, options): + def eval(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: @@ -1965,6 +1812,46 @@ def apply(self, graph: Expression, what, expression, evaluation, options): return mathics_graph.add_vertices(*zip(*[_parse_item(what)])) +class VertexConnectivity(_NetworkXBuiltin): + """ + >> VertexConnectivity[{1 <-> 2, 2 <-> 3}] + = 1 + + >> VertexConnectivity[{1 -> 2, 2 -> 3}] + = 0 + + >> VertexConnectivity[{1 -> 2, 2 -> 3, 3 -> 1}] + = 1 + + >> VertexConnectivity[{1 <-> 2, 2 <-> 3, 1 <-> 3}] + = 2 + + >> VertexConnectivity[{1 <-> 2, 3 <-> 4}] + = 0 + + #> VertexConnectivity[Graph[{}]] + = VertexConnectivity[-Graph-] + """ + + def eval(self, graph, expression, evaluation, options): + "%(name)s[graph_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph and not graph.empty(): + if not _is_connected(graph.G): + return Integer(0) + else: + return Integer(nx.node_connectivity(graph.G)) + + def eval_st(self, graph, s, t, expression, evaluation, options): + "%(name)s[graph_, s_, t_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph and not graph.empty(): + if not _is_connected(graph.G): + return Integer(0) + else: + return Integer(nx.node_connectivity(graph.G, s, t)) + + class VertexDelete(_NetworkXBuiltin): """ >> g1 = Graph[{1 -> 2, 2 -> 3, 3 -> 4}]; @@ -1976,7 +1863,7 @@ class VertexDelete(_NetworkXBuiltin): = -Graph- """ - def apply(self, graph, what, expression, evaluation, options): + def eval(self, graph, what, expression, evaluation, options): "%(name)s[graph_, what_, OptionsPattern[%(name)s]]" graph = self._build_graph(graph, evaluation, options, expression) if graph: @@ -1995,13 +1882,68 @@ def apply(self, graph, what, expression, evaluation, options): return graph.delete_vertices([what]) +class VertexIndex(_NetworkXBuiltin): + """ + >> VertexIndex[{c <-> d, d <-> a}, a] + = 3 + """ + + def eval(self, graph, v, expression, evaluation, options): + "%(name)s[graph_, v_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + i = graph.vertices.get_index().get(v) + if i is None: + self._not_a_vertex(expression, 2, evaluation) + else: + return Integer(i + 1) + + +class VertexList(_PatternList): + """ + >> VertexList[{1 -> 2, 2 -> 3}] + = {1, 2, 3} + + >> VertexList[{a -> c, c -> b}] + = {a, c, b} + + >> VertexList[{a -> c, 5 -> b}, _Integer -> 10] + = {10} + """ + + def _items(self, graph): + return graph.vertices + + +class UndirectedEdge(Builtin): + """ +
+
'UndirectedEdge[$u$, $v$]' +
create an undirected edge between $u$ and $v$. +
+ + >> a <-> b + = UndirectedEdge[a, b] + + >> (a <-> b) <-> c + = UndirectedEdge[UndirectedEdge[a, b], c] + + >> a <-> (b <-> c) + = UndirectedEdge[a, UndirectedEdge[b, c]] + """ + + summary_text = "makes undirected graph edge" + + pass + + # class EdgeAdd(_NetworkXBuiltin): # """ # >> EdgeAdd[{1->2,2->3},3->1] # = -Graph- # """ -# def apply(self, graph: Expression, what, expression, evaluation, options): +# def eval(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: @@ -2032,7 +1974,7 @@ def apply(self, graph, what, expression, evaluation, options): # = -Graph- # """ -# def apply(self, graph, what, expression, evaluation, options): +# def eval(self, graph, what, expression, evaluation, options): # "%(name)s[graph_, what_, OptionsPattern[%(name)s]]" # graph = self._build_graph(graph, evaluation, options, expression) # if graph: diff --git a/pymathics/graph/generators.py b/pymathics/graph/generators.py index 1e54a2e..f5ffa78 100644 --- a/pymathics/graph/generators.py +++ b/pymathics/graph/generators.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- """ -Routines for generating classes of Graphs. - -networkx does all the heavy lifting. +Routines for generating classes of Graphs """ from typing import Callable, Optional from mathics.builtin.numbers.randomnumbers import RandomEnv -from mathics.core.expression import Expression, Integer, String +from mathics.core.atoms import Integer, String +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression -from pymathics.graph.__main__ import ( +from pymathics.graph.base import ( Graph, SymbolUndirectedEdge, _convert_networkx_graph, @@ -25,6 +25,80 @@ # TODO: Can this code can be DRY'd more? +def eval_complete_graph( + self, n: Integer, expression, evaluation: Evaluation, options: dict +) -> Optional[Graph]: + py_n = n.value + + if py_n < 1: + evaluation.message(self.get_name(), "ilsmp", expression) + return + + args = (py_n,) + g = graph_helper( + nx.complete_graph, options, False, "circular", evaluation, None, *args + ) + if not g: + return None + + g.G.n = n + return g + + +def eval_full_rary_tree( + self, r: Integer, n: Integer, expression, evaluation: Evaluation, options: dict +) -> Optional[Graph]: + """ + Call networkx to get a full_raray_tree using parameters, ``r`` and ``t``. + """ + py_r = r.value + + if py_r < 0: + evaluation.message(self.get_name(), "ilsmp", expression) + return + + py_n = n.value + if py_n < 0: + evaluation.message(self.get_name(), "ilsmp", expression) + return + + args = (py_r, py_n) + g = graph_helper(nx.full_rary_tree, options, True, "tree", evaluation, 0, *args) + if not g: + return None + + g.G.r = r + g.G.n = n + return g + + +def eval_hkn_harary( + self, k: Integer, n: Integer, expression, evaluation: Evaluation, options: dict +) -> Optional[Graph]: + py_k = k.value + + if py_k < 0: + evaluation.message(self.get_name(), "ilsmp", expression) + return + + py_n = n.value + if py_n < 0: + evaluation.message(self.get_name(), "ilsmp2", expression) + return + + from pymathics.graph.harary import hkn_harary_graph + + args = (py_k, py_n) + g = graph_helper( + hkn_harary_graph, options, False, "circular", evaluation, None, *args + ) + if not g: + return None + g.k = py_k + g.n = py_n + return g + + def graph_helper( graph_generator_func: Callable, options: dict, @@ -79,15 +153,17 @@ class BalancedTree(_NetworkXBuiltin): options = DEFAULT_TREE_OPTIONS - def apply(self, r, h, expression, evaluation, options): + def eval( + self, r: Integer, h: Integer, expression, evaluation: Evaluation, options: dict + ) -> Optional[Graph]: "%(name)s[r_Integer, h_Integer, OptionsPattern[%(name)s]]" - py_r = r.get_int_value() + py_r = r.value if py_r < 0: evaluation.message(self.get_name(), "ilsmp", expression) return None - py_h = h.get_int_value() + py_h = h.value if py_h < 0: evaluation.message(self.get_name(), "ilsmp2", expression) return None @@ -108,7 +184,7 @@ class BarbellGraph(_NetworkXBuiltin):
Barbell Graph: two complete graphs connected by a path. - ## >> BarBellGraph[4, 1] + ## >> BarbellGraph[4, 1] ## = -Graph- """ @@ -118,8 +194,15 @@ class BarbellGraph(_NetworkXBuiltin): "ilsmp2": "Expected a non-negative integer at position 2 in ``.", } - def apply(self, m1, m2, expression, evaluation, options): - "%(name)s[m1_Integer, m2_Integer, OptionsPattern[%(name)s]]" + def eval( + self, + m1: Integer, + m2: Integer, + expression, + evaluation: Evaluation, + options: dict, + ) -> Optional[Graph]: + "BarbellGraph[m1_Integer, m2_Integer, OptionsPattern[BarbellGraph]]" py_m1 = m1.value if py_m1 < 0: @@ -128,7 +211,7 @@ def apply(self, m1, m2, expression, evaluation, options): py_m2 = m2.value if py_m2 < 0: - evaluation.message(self.get_name(), "ilsmp", expression) + evaluation.message(self.get_name(), "ilsmp2", expression) return args = (py_m1, py_m2) @@ -145,7 +228,7 @@ def apply(self, m1, m2, expression, evaluation, options): # This code will be in the 2.6 release of networkx. # See https://github.com/networkx/networkx/pull/4461 -def binomial_tree(n, create_using=None): +def binomial_tree(n, create_using=None) -> Graph: """Returns the Binomial Tree of order n. The binomial tree of order 0 consists of a single vertex. A binomial tree of order k @@ -207,9 +290,11 @@ class BinomialTree(_NetworkXBuiltin): "mem": "Out of memory", } - def apply(self, n, expression, evaluation, options): + def eval( + self, n: Integer, expression, evaluation: Evaluation, options: dict + ) -> Graph: "%(name)s[n_Integer, OptionsPattern[%(name)s]]" - py_n = n.get_int_value() + py_n = n.value if py_n < 0: evaluation.message(self.get_name(), "ilsmp", expression) @@ -223,24 +308,6 @@ def apply(self, n, expression, evaluation, options): return g -def complete_graph_apply(self, n, expression, evaluation, options): - py_n = n.get_int_value() - - if py_n < 1: - evaluation.message(self.get_name(), "ilsmp", expression) - return - - args = (py_n,) - g = graph_helper( - nx.complete_graph, options, False, "circular", evaluation, None, *args - ) - if not g: - return None - - g.G.n = n - return g - - class CompleteGraph(_NetworkXBuiltin): """
@@ -260,11 +327,11 @@ class CompleteGraph(_NetworkXBuiltin): "ilsmp": "Expected a positive integer at position 1 in ``.", } - def apply(self, n, expression, evaluation, options): + def eval(self, n: Integer, expression, evaluation: Evaluation, options: dict): "%(name)s[n_Integer, OptionsPattern[%(name)s]]" - return complete_graph_apply(self, n, expression, evaluation, options) + return eval_complete_graph(self, n, expression, evaluation, options) - def apply_multipartite(self, n, evaluation, options): + def eval_multipartite(self, n, evaluation: Evaluation, options: dict): "%(name)s[n_List, OptionsPattern[%(name)s]]" if all(isinstance(i, Integer) for i in n.leaves): return Graph( @@ -291,23 +358,25 @@ class CompleteKaryTree(_NetworkXBuiltin): options = DEFAULT_TREE_OPTIONS - def apply(self, k, n, expression, evaluation, options): + def eval(self, k, n, expression, evaluation: Evaluation, options: dict): "%(name)s[n_Integer, k_Integer, OptionsPattern[%(name)s]]" n_int = n.get_int_value() k_int = k.get_int_value() new_n_int = int(((k_int**n_int) - 1) / (k_int - 1)) - return f_r_t_apply(self, k, Integer(new_n_int), expression, evaluation, options) + return eval_full_rary_tree( + self, k, Integer(new_n_int), expression, evaluation, options + ) # FIXME: can be done with rules? - def apply_2(self, n, expression, evaluation, options): + def eval_2(self, n, expression, evaluation: Evaluation, options: dict): "%(name)s[n_Integer, OptionsPattern[%(name)s]]" n_int = n.get_int_value() new_n_int = int(2**n_int) - 1 - return f_r_t_apply( + return eval_full_rary_tree( self, Integer(2), Integer(new_n_int), expression, evaluation, options ) @@ -322,37 +391,13 @@ class CycleGraph(_NetworkXBuiltin): = -Graph- """ - def apply(self, n, expression, evaluation, options): + def eval(self, n: Integer, expression, evaluation: Evaluation, options: dict): "%(name)s[n_Integer, OptionsPattern[%(name)s]]" n_int = n.get_int_value() if n_int < 3: - return complete_graph_apply(self, n, expression, evaluation, options) + return eval_complete_graph(self, n, expression, evaluation, options) else: - return hkn_harary_apply( - self, Integer(2), n, expression, evaluation, options - ) - - -def f_r_t_apply(self, r, n, expression, evaluation, options): - py_r = r.get_int_value() - - if py_r < 0: - evaluation.message(self.get_name(), "ilsmp", expression) - return - - py_n = n.get_int_value() - if py_n < 0: - evaluation.message(self.get_name(), "ilsmp", expression) - return - - args = (py_r, py_n) - g = graph_helper(nx.full_rary_tree, options, True, "tree", evaluation, 0, *args) - if not g: - return None - - g.G.r = r - g.G.n = n - return g + return eval_hkn_harary(self, Integer(2), n, expression, evaluation, options) class FullRAryTree(_NetworkXBuiltin): @@ -378,9 +423,9 @@ class FullRAryTree(_NetworkXBuiltin): options = DEFAULT_TREE_OPTIONS - def apply(self, r, n, expression, evaluation, options): + def eval(self, r, n, expression, evaluation: Evaluation, options: dict): "%(name)s[r_Integer, n_Integer, OptionsPattern[%(name)s]]" - return f_r_t_apply(self, r, n, expression, evaluation, options) + return eval_full_rary_tree(self, r, n, expression, evaluation, options) class GraphAtlas(_NetworkXBuiltin): @@ -399,7 +444,7 @@ class GraphAtlas(_NetworkXBuiltin): "ilsmp": "Expected a positive integer at position 1 in ``.", } - def apply(self, n, expression, evaluation, options): + def eval(self, n, expression, evaluation: Evaluation, options: dict): "%(name)s[n_Integer, OptionsPattern[%(name)s]]" py_n = n.get_int_value() @@ -417,31 +462,6 @@ def apply(self, n, expression, evaluation, options): return g -def hkn_harary_apply(self, k, n, expression, evaluation, options): - py_k = k.get_int_value() - - if py_k < 0: - evaluation.message(self.get_name(), "ilsmp", expression) - return - - py_n = n.get_int_value() - if py_n < 0: - evaluation.message(self.get_name(), "ilsmp2", expression) - return - - from pymathics.graph.harary import hkn_harary_graph - - args = (py_k, py_n) - g = graph_helper( - hkn_harary_graph, options, False, "circular", evaluation, None, *args - ) - if not g: - return None - g.k = py_k - g.n = py_n - return g - - class HknHararyGraph(_NetworkXBuiltin): """
'HmnHararyGraph[$k$, $n$]' @@ -464,9 +484,9 @@ class HknHararyGraph(_NetworkXBuiltin): "ilsmp2": "Expected a non-negative integer at position 2 in ``.", } - def apply(self, k, n, expression, evaluation, options): + def eval(self, k, n, expression, evaluation: Evaluation, options: dict): "%(name)s[k_Integer, n_Integer, OptionsPattern[%(name)s]]" - return hkn_harary_apply(self, k, n, expression, evaluation, options) + return eval_hkn_harary(self, k, n, expression, evaluation, options) class HmnHararyGraph(_NetworkXBuiltin): @@ -490,7 +510,7 @@ class HmnHararyGraph(_NetworkXBuiltin): "ilsmp2": "Expected a non-negative integer at position 2 in ``.", } - def apply(self, n, m, expression, evaluation, options): + def eval(self, n, m, expression, evaluation: Evaluation, options: dict): "%(name)s[n_Integer, m_Integer, OptionsPattern[%(name)s]]" py_n = n.get_int_value() @@ -543,13 +563,15 @@ class KaryTree(_NetworkXBuiltin): options = DEFAULT_TREE_OPTIONS - def apply(self, n, expression, evaluation, options): - "%(name)s[n_Integer, OptionsPattern[%(name)s]]" - return f_r_t_apply(self, Integer(2), n, expression, evaluation, options) + def eval(self, n, expression, evaluation: Evaluation, options: dict) -> Graph: + "KaryTree[n_Integer, OptionsPattern[KaryTree]]" + return eval_full_rary_tree(self, Integer(2), n, expression, evaluation, options) - def apply_2(self, n, k, expression, evaluation, options): - "%(name)s[n_Integer, k_Integer, OptionsPattern[%(name)s]]" - return f_r_t_apply(self, k, n, expression, evaluation, options) + def eval_with_k( + self, n, k, expression, evaluation: Evaluation, options: dict + ) -> Graph: + "KaryTree[n_Integer, k_Integer, OptionsPattern[KaryTree]]" + return eval_full_rary_tree(self, k, n, expression, evaluation, options) class LadderGraph(_NetworkXBuiltin): @@ -559,7 +581,7 @@ class LadderGraph(_NetworkXBuiltin):
Returns the Ladder graph of length $n$.
- >> StarGraph[8] + >> LadderGraph[8] = -Graph- """ @@ -567,9 +589,11 @@ class LadderGraph(_NetworkXBuiltin): "ilsmp": "Expected a positive integer at position 1 in ``.", } - def apply(self, n, expression, evaluation, options): - "%(name)s[n_Integer, OptionsPattern[%(name)s]]" - py_n = n.get_int_value() + def eval( + self, n: Integer, expression, evaluation: Evaluation, options: dict + ) -> Graph: + "LadderGraph[n_Integer, OptionsPattern[%(name)s]]" + py_n = n.value if py_n < 1: evaluation.message(self.get_name(), "ilsmp", expression) @@ -595,9 +619,9 @@ class PathGraph(_NetworkXBuiltin): = -Graph- """ - def apply(self, element, evaluation, options): - "PathGraph[l_List, OptionsPattern[%(name)s]]" - elements = element.elements + def eval(self, e, evaluation: Evaluation, options: dict) -> Graph: + "PathGraph[e_List, OptionsPattern[PathGraph]]" + elements = e.elements def edges(): for u, v in zip(elements, elements[1:]): @@ -621,10 +645,12 @@ class RandomGraph(_NetworkXBuiltin):
""" - def _generate(self, n, m, k, evaluation, options): - py_n = n.get_int_value() - py_m = m.get_int_value() - py_k = k.get_int_value() + def _generate( + self, n: Integer, m: Integer, k: Integer, evaluation: Evaluation, options: dict + ) -> Graph: + py_n = n.value + py_m = m.value + py_k = k.value is_directed = has_directed_option(options) with RandomEnv(evaluation) as _: @@ -633,13 +659,15 @@ def _generate(self, n, m, k, evaluation, options): G = nx.gnm_random_graph(py_n, py_m, directed=is_directed) yield _convert_networkx_graph(G, options) - def apply_nm(self, n, m, expression, evaluation, options): + def eval_nm(self, n, m, expression, evaluation: Evaluation, options: dict) -> Graph: "%(name)s[{n_Integer, m_Integer}, OptionsPattern[%(name)s]]" g = list(self._generate(n, m, Integer(1), evaluation, options))[0] _process_graph_options(g, options) return g - def apply_nmk(self, n, m, k, expression, evaluation, options): + def eval_nmk( + self, n, m, k, expression, evaluation: Evaluation, options: dict + ) -> Graph: "%(name)s[{n_Integer, m_Integer}, k_Integer, OptionsPattern[%(name)s]]" return Expression("List", *self._generate(n, m, k, evaluation, options)) @@ -660,9 +688,11 @@ class RandomTree(_NetworkXBuiltin): "ilsmp": "Expected a non-negative integer at position 1 in ``.", } - def apply(self, n, expression, evaluation, options): - "%(name)s[n_Integer, OptionsPattern[%(name)s]]" - py_n = n.get_int_value() + def eval( + self, n: Integer, expression, evaluation: Evaluation, options: dict + ) -> Graph: + "RandomTree[n_Integer, OptionsPattern[RandomTree]]" + py_n = n.value if py_n < 0: evaluation.message(self.get_name(), "ilsmp", expression) @@ -691,9 +721,11 @@ class StarGraph(_NetworkXBuiltin): "ilsmp": "Expected a positive integer at position 1 in ``.", } - def apply(self, n, expression, evaluation, options): - "%(name)s[n_Integer, OptionsPattern[%(name)s]]" - py_n = n.get_int_value() + def eval( + self, n: Integer, expression, evaluation: Evaluation, options: dict + ) -> Graph: + "StarGraph[n_Integer, OptionsPattern[StarGraph]]" + py_n = n.value if py_n < 1: evaluation.message(self.get_name(), "ilsmp", expression) @@ -726,7 +758,7 @@ class GraphData(_NetworkXBuiltin): >> GraphData["PappusGraph"] """ - def apply(self, name, expression, evaluation, options): + def eval(self, name, expression, evaluation: Evaluation, options: dict) -> Graph: "%(name)s[name_String, OptionsPattern[%(name)s]]" py_name = name.get_string_value() fn, layout = WL_TO_NETWORKX_FN.get(py_name, (None, None)) diff --git a/pymathics/graph/measures_and_metrics.py b/pymathics/graph/measures_and_metrics.py new file mode 100644 index 0000000..468df6c --- /dev/null +++ b/pymathics/graph/measures_and_metrics.py @@ -0,0 +1,153 @@ +""" +Graph Measures and Metrics + +Measures include basic measures, such as the number of vertices and edges, \ +connectivity, degree measures, centrality, and so on. +""" + + +from typing import Optional + +from mathics.core.atoms import Integer +from mathics.core.convert.expression import ListExpression +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol +from mathics.core.systemsymbols import SymbolLength + +from pymathics.graph.base import _NetworkXBuiltin + +# FIXME: add context +SymbolCases = Symbol("Cases") + + +# FIXME put this in its own file/module basic +# when pymathics doc can handle this. +# """ +# Basic Graph Measures +# """ +class _PatternCount(_NetworkXBuiltin): + """ + Counts of vertices or edges, allowing rules to specify the graph. + """ + + no_doc = True + + def eval(self, graph, expression, evaluation, options) -> Optional[Integer]: + "%(name)s[graph_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + return Integer(len(self._items(graph))) + + def eval_patt( + self, graph, patt, expression, evaluation, options + ) -> Optional[Expression]: + "%(name)s[graph_, patt_, OptionsPattern[%(name)s]]" + graph = self._build_graph(graph, evaluation, options, expression) + if graph: + return Expression( + SymbolLength, + Expression(SymbolCases, ListExpression(*self._items(graph)), patt), + ) + + +class EdgeCount(_PatternCount): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/EdgeCount.html + +
+
'EdgeCount[$g$]' +
returns a count of the number of edges in graph $g$. + +
'EdgeCount[$g$, $patt$]' +
returns the number of edges that match the pattern $patt$. + +
'EdgeCount[{$v$->$w}, ...}, ...]' +
uses rules $v$->$w$ to specify the graph $g$. +
+ + >> EdgeCount[{1 -> 2, 2 -> 3}] + = 2 + """ + + no_doc = False + summary_text = "count edges in graph" + + def _items(self, graph): + return graph.G.edges + + +class VertexCount(_PatternCount): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/VertexCount.html + +
+
'VertexCount[$g$]' +
returns a count of the number of vertices in graph $g$. + +
'VertexCount[$g$, $patt$]' +
returns the number of vertices that match the pattern $patt$. + +
'VertexCount[{$v$->$w}, ...}, ...]' +
uses rules $v$->$w$ to specify the graph $g$. +
+ + >> VertexCount[{1 -> 2, 2 -> 3}] + = 3 + + >> VertexCount[{1 -> x, x -> 3}, _Integer] + = 2 + """ + + no_doc = False + summary_text = "count vertices in graph" + + def _items(self, graph): + return graph.G.nodes + + +# Put this in its own file/module "degree.py" +# when pymathics doc can handle. +# """ +# Graph Degree Measures +# """ + + +class VertexDegree(_NetworkXBuiltin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/EdgeCount.html + +
+
'VertexDegree[$g$]' +
returns a list of the degrees of each of the vertices in graph $g$. + +
'EdgeCount[$g$, $patt$]' +
returns the number of edges that match the pattern $patt$. + +
'EdgeCount[{$v$->$w}, ...}, ...]' +
uses rules $v$->$w$ to specify the graph $g$. +
+ + >> VertexDegree[{1 <-> 2, 2 <-> 3, 2 <-> 4}] + = {1, 3, 1, 1} + """ + + no_doc = False + summary_text = "list graph vertex degrees" + + def eval(self, graph, evaluation, options): + "%(name)s[graph_, OptionsPattern[%(name)s]]" + + def degrees(graph): + degrees = dict(list(graph.G.degree(graph.vertices))) + return ListExpression(*[Integer(degrees.get(v, 0)) for v in graph.vertices]) + + return self._evaluate_atom(graph, options, degrees) + + +# TODO: VertexInDegree, VertexOutDegree diff --git a/pymathics/graph/tree.py b/pymathics/graph/tree.py index 93a2218..6261a42 100644 --- a/pymathics/graph/tree.py +++ b/pymathics/graph/tree.py @@ -1,7 +1,9 @@ import networkx as nx -from mathics.core.expression import Atom, Symbol +from mathics.core.atoms import Atom +from mathics.core.evaluation import Evaluation +from mathics.core.symbols import SymbolConstant, SymbolFalse, SymbolTrue -from pymathics.graph.__main__ import ( +from pymathics.graph.base import ( DEFAULT_GRAPH_OPTIONS, Graph, _graph_from_list, @@ -16,13 +18,19 @@ from mathics.builtin.base import AtomBuiltin -class TreeGraphAtom(AtomBuiltin): +def eval_TreeGraphQ(g: Graph) -> SymbolConstant: """ - >> TreeGraph[{1->2, 2->3, 3->1}] - = -Graph- - + Returns SymbolTrue if g is a (networkx) tree and SymbolFalse + otherwise. """ + if not isinstance(g, Graph): + return SymbolFalse + return SymbolTrue if nx.is_tree(g.G) else SymbolFalse + +# FIXME: do we need to have TreeGraphAtom and TreeGraph? +# Can't these be combined into one? +class TreeGraphAtom(AtomBuiltin): options = DEFAULT_TREE_OPTIONS messages = { @@ -30,9 +38,9 @@ class TreeGraphAtom(AtomBuiltin): "notree": "Graph is not a tree.", } - def apply(self, rules, evaluation, options): + def eval(self, rules, evaluation: Evaluation, options: dict): "TreeGraph[rules_List, OptionsPattern[%(name)s]]" - g = _graph_from_list(rules.leaves, options) + g = _graph_from_list(rules.elements, options) if not nx.is_tree(g.G): evaluation.message(self.get_name(), "notree") @@ -40,13 +48,13 @@ def apply(self, rules, evaluation, options): # Compute/check/set for root? return g - def apply_1(self, vertices, edges, evaluation, options): + def eval_with_v_e(self, vertices, edges, evaluation: Evaluation, options: dict): "TreeGraph[vertices_List, edges_List, OptionsPattern[%(name)s]]" - if not all(isinstance(v, Atom) for v in vertices.leaves): + if not all(isinstance(v, Atom) for v in vertices.elements): evaluation.message(self.get_name(), "v") g = _graph_from_list( - edges.leaves, options=options, new_vertices=vertices.leaves + edges.elements, options=options, new_vertices=vertices.elements ) if not nx.is_tree(g.G): evaluation.message(self.get_name(), "notree") @@ -57,6 +65,12 @@ def apply_1(self, vertices, edges, evaluation, options): class TreeGraph(Graph): + """ + >> TreeGraph[{1->2, 2->3, 2->4}] + = -Graph- + + """ + options = DEFAULT_TREE_OPTIONS messages = { @@ -85,8 +99,8 @@ class TreeGraphQ(_NetworkXBuiltin): = False """ - def apply(self, g, expression, evaluation, options): + def eval( + self, g, expression, evaluation: Evaluation, options: dict + ) -> SymbolConstant: "TreeGraphQ[g_, OptionsPattern[%(name)s]]" - if not isinstance(g, Graph): - return Symbol("False") - return Symbol("True" if nx.is_tree(g.G) else "False") + return eval_TreeGraphQ(g) diff --git a/pymathics/graph/version.py b/pymathics/graph/version.py index 769c79e..959c480 100644 --- a/pymathics/graph/version.py +++ b/pymathics/graph/version.py @@ -5,4 +5,4 @@ # well as importing into Python. That's why there is no # space around "=" below. # fmt: off -__version__="5.0.0a0" # noqa +__version__="6.0.0a0" # noqa diff --git a/setup.py b/setup.py index b675b9b..1b5cff6 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ from setuptools import setup, find_namespace_packages # Ensure user has the correct Python version -if sys.version_info < (3, 6): - print("Mathics support Python 3.6 and above; you have %d.%d" % sys.version_info[:2]) +if sys.version_info < (3, 7): + print("Mathics support Python 3.7 and above; you have %d.%d" % sys.version_info[:2]) sys.exit(-1) @@ -31,7 +31,7 @@ def read(*rnames): name="pymathics-graph", version=__version__, # noqa packages=find_namespace_packages(include=["pymathics.*"]), - install_requires=["Mathics3>=5.0.0", "networkx>=2.8.0", "pydot", "matplotlib"], + install_requires=["Mathics3>5.1.0", "networkx>=2.8.0", "pydot", "matplotlib"], # don't pack Mathics in egg because of media files, etc. zip_safe=False, maintainer="Mathics Group", @@ -44,7 +44,6 @@ def read(*rnames): "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Programming Language :: Python", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/test/helper.py b/test/helper.py new file mode 100644 index 0000000..ed42c82 --- /dev/null +++ b/test/helper.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +import time +from typing import Optional + +from mathics.session import MathicsSession + +session = MathicsSession(add_builtin=True, catch_interrupt=False) + +# Set up a Mathics3 session with definitions. +# For consistency set the character encoding ASCII which is +# the lowest common denominator available on all systems. +session = MathicsSession(character_encoding="ASCII") + + +def reset_session(add_builtin=True, catch_interrupt=False): + global session + session.reset() + + +def evaluate_value(str_expr: str): + return session.evaluate(str_expr).value + + +def evaluate(str_expr: str): + return session.evaluate(str_expr) + + +def check_evaluation( + str_expr: str, + str_expected: str, + failure_message: str = "", + hold_expected: bool = False, + to_string_expr: bool = True, + to_string_expected: bool = True, + to_python_expected: bool = False, + expected_messages: Optional[tuple] = None, +): + """ + Helper function to test Mathics expression against + its results + + Compares the expressions represented by ``str_expr`` and ``str_expected`` by evaluating + the first, and optionally, the second. + + to_string_expr: If ``True`` (default value) the result of the evaluation is converted + into a Python string. Otherwise, the expression is kept as an Expression + object. If this argument is set to ``None``, the session is reset. + + failure_message (str): message shown in case of failure + hold_expected (bool): If ``False`` (default value) the ``str_expected`` is evaluated. Otherwise, + the expression is considered literally. + + to_string_expected: If ``True`` (default value) the expected expression is + evaluated and then converted to a Python string. result of the evaluation is converted + into a Python string. If ``False``, the expected expression is kept as an Expression object. + + to_python_expected: If ``True``, and ``to_string_expected`` is ``False``, the result of evaluating ``str_expr`` + is compared against the result of the evaluation of ``str_expected``, converted into a + Python object. + + expected_messages ``Optional[tuple[str]]``: If a tuple of strings are passed into this parameter, messages and prints raised during + the evaluation of ``str_expr`` are compared with the elements of the list. If ``None``, this comparison + is ommited. + """ + if str_expr is None: + reset_session() + return + + if to_string_expr: + str_expr = f"ToString[{str_expr}]" + result = evaluate_value(str_expr) + else: + result = evaluate(str_expr) + + outs = [out.text for out in session.evaluation.out] + + if to_string_expected: + if hold_expected: + expected = str_expected + else: + str_expected = f"ToString[{str_expected}]" + expected = evaluate_value(str_expected) + else: + if hold_expected: + if to_python_expected: + expected = str_expected + else: + expected = evaluate(f"HoldForm[{str_expected}]").elements[0] + else: + expected = evaluate(str_expected) + if to_python_expected: + expected = expected.to_python(string_quotes=False) + + print(time.asctime()) + if failure_message: + print((result, expected)) + assert result == expected, failure_message + else: + print((result, expected)) + assert result == expected + + if expected_messages is not None: + msgs = list(expected_messages) + expected_len = len(msgs) + got_len = len(outs) + assert ( + expected_len == got_len + ), f"expected {expected_len}; got {got_len}. Messages: {outs}" + for (out, msg) in zip(outs, msgs): + if out != msg: + print(f"out:<<{out}>>") + print(" and ") + print(f"expected=<<{msg}>>") + assert False, " do not match." diff --git a/test/test_algorithms.py b/test/test_algorithms.py new file mode 100644 index 0000000..8c784b3 --- /dev/null +++ b/test/test_algorithms.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for pymathics.graph.algorithms +""" +from test.helper import check_evaluation, evaluate, evaluate_value + + +def setup_module(module): + """Load pymathics.graph""" + assert evaluate_value('LoadModule["pymathics.graph"]') == "pymathics.graph" + evaluate("SortList[list_] := Sort[Map[Sort, list]]") + + +def test_connected_components(): + for str_expr, str_expected in [ + ("g = Graph[{1 -> 2, 2 -> 3, 3 <-> 4}];", "Null"), + ("SortList[ConnectedComponents[g]]", "{{1}, {2}, {3, 4}}"), + ("g = Graph[{1 -> 2, 2 -> 3, 3 -> 1}];", "Null"), + ("SortList[ConnectedComponents[g]]", "{{1, 2, 3}}"), + ]: + check_evaluation(str_expr, str_expected) + + +def test_graph_distance(): + for str_expr, str_expected in [ + ("GraphDistance[{1 <-> 2, 2 <-> 3, 3 <-> 4, 2 <-> 4, 4 -> 5}, 1, 5]", "3"), + ("GraphDistance[{1 <-> 2, 2 <-> 3, 3 <-> 4, 4 -> 2, 4 -> 5}, 1, 5]", "4"), + # ("GraphDistance[{1 <-> 2, 2 <-> 3, 4 -> 3, 4 -> 2, 4 -> 5}, 1, 5]", "Infinity"), + ( + "Sort[GraphDistance[{1 <-> 2, 2 <-> 3, 3 <-> 4, 2 <-> 4, 4 -> 5}, 3]]", + "{0, 1, 1, 2, 2}", + ), + ]: + check_evaluation(str_expr, str_expected) diff --git a/test/test_tree.py b/test/test_tree.py new file mode 100644 index 0000000..06f6dcd --- /dev/null +++ b/test/test_tree.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for pymathics.graph.tree +""" +from test.helper import check_evaluation, evaluate_value + + +def setup_module(module): + """Load pymathics.graph""" + assert evaluate_value('LoadModule["pymathics.graph"]') == "pymathics.graph" + + +def test_tree(): + for str_expr, str_expected in [ + ("TreeGraphQ[StarGraph[3]]", "True"), + ("TreeGraphQ[CompleteGraph[0]]", "False"), + ("TreeGraphQ[CompleteGraph[1]]", "True"), + ("TreeGraphQ[CompleteGraph[2]]", "True"), + ("TreeGraphQ[CompleteGraph[3]]", "False"), + ]: + check_evaluation(str_expr, str_expected)