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)