Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 53 additions & 66 deletions src/pgraph/PGraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,7 @@ def plot(
ax.text(vertex.x, vertex.y, f" {vertex.name}", **textopt)
if colorcomponents:
ax.plot(vertex.x, vertex.y, color=color[c, :], **vopt)
for v in vertex.neighbours():
for v in vertex.adjacent():
ax.plot(
[vertex.x, v.x],
[vertex.y, v.y],
Expand All @@ -1247,7 +1247,7 @@ def plot(
)
else:
ax.plot(vertex.x, vertex.y, **vopt)
for v in vertex.neighbours():
for v in vertex.adjacent():
ax.plot([vertex.x, v.x], [vertex.y, v.y], **eopt)
else:
# 3D or higher plotting, just do (x, y, z)
Expand All @@ -1267,7 +1267,7 @@ def plot(
vertex.z,
**{**dict(color=color[c, :]), **vopt},
)
for v in vertex.neighbours():
for v in vertex.adjacent():
ax.plot(
[vertex.x, v.x],
[vertex.y, v.y],
Expand All @@ -1276,7 +1276,7 @@ def plot(
)
else:
ax.plot(vertex.x, vertex.y, vertex.z, **vopt)
for v in vertex.neighbours():
for v in vertex.adjacent():
ax.plot(
[vertex.x, v.x],
[vertex.y, v.y],
Expand Down Expand Up @@ -1669,7 +1669,7 @@ def iscyclic(self) -> bool:
repeatedly remove vertices with no remaining incoming edges. If every
vertex can eventually be removed, the graph is a DAG (acyclic); if
some are never freed, they form a cycle. This is O(V+E), using
vertices' outgoing edges via :meth:`BaseVertex.neighbours` the same
vertices' outgoing edges via :meth:`BaseVertex.adjacent` the same
way :meth:`path_BFS` and friends do.

.. note:: A self-loop is a cycle of length one, so it makes either kind
Expand Down Expand Up @@ -1713,7 +1713,7 @@ def iscyclic(self) -> bool:
while frontier:
vertex = frontier.pop()
removed += 1
for n in vertex.neighbours():
for n in vertex.adjacent():
indegree[n] -= 1
if indegree[n] == 0:
frontier.append(n)
Expand Down Expand Up @@ -1950,7 +1950,7 @@ def adjacency(self) -> NDArray:

A = np.zeros((self.n, self.n))
for vertex in self:
for n in vertex.neighbours():
for n in vertex.adjacent():
A[vdict[vertex], vdict[n]] += 1
return A

Expand Down Expand Up @@ -2185,7 +2185,7 @@ def path_BFS(
print(" expand", x.name)

# expand the vertex
for n in x.neighbours():
for n in x.adjacent():
if n is x:
continue # self-loop, never part of a shortest path
if n is G:
Expand Down Expand Up @@ -2526,7 +2526,7 @@ def _graphcolor(self) -> None:
while len(q) > 0:
v = q.pop() # expand v
v.label = label
for n in v.neighbours():
for n in v.adjacent():
if n.label is None:
q.append(n)
lastlabel = label
Expand Down Expand Up @@ -2580,7 +2580,7 @@ def _graphcolor(self) -> int | None:
for v in self:
if v.label is None:
# no label, try to inherit one from a neighbour
for n in v.neighbours():
for n in v.adjacent():
if n.label is not None:
# neighbour has a label
v.label = n.label
Expand All @@ -2595,7 +2595,7 @@ def _graphcolor(self) -> int | None:
assert label is not None

# now look for clashes
for n in v.neighbours():
for n in v.adjacent():
if n.label is None:
# neighbour has no label, give it this one
n.label = label
Expand Down Expand Up @@ -3029,17 +3029,28 @@ def copy(self, cls: type[_BaseGraph] | None = None) -> BaseVertex:
else:
return self.__class__(coord=self.coord, name=self.name)

def neighbours(self) -> list[BaseVertex]:
def adjacent(self) -> list[BaseVertex]:
"""
Neighbours of a vertex
Vertices adjacent to this vertex

:return: the vertices at the far end of this vertex's edges
:rtype: list of BaseVertex subclass

``v.neighbours()`` is a list of neighbours of this vertex.
``v.adjacent()`` is a list of the vertices that this vertex is joined
to by an edge. "Adjacent" is the graph-theory term, and it is the
relation that :meth:`_BaseGraph.adjacency` tabulates: row ``i`` of the
adjacency matrix is non-zero exactly at the columns of the vertices
returned by ``g[i].adjacent()``.

.. note:: For a directed graph the neighbours are those on edges leaving this vertex
.. note::

.. note:: There is one entry per edge, so a neighbour joined by ``k``
parallel edges appears ``k`` times. In an undirected graph a
self-loop lists the vertex itself twice, once for each end.
- For a directed graph these are the vertices at the head of the
edges leaving this vertex, the *successors*.
- There is one entry per edge, so a vertex joined by ``k`` parallel
edges appears ``k`` times. In an undirected graph a self-loop
lists the vertex itself twice, once for each end.
- ``neighbours`` and ``neighbors`` are aliases, the same method
under the British and American spellings, and are not deprecated.

.. runblock:: pycon

Expand All @@ -3050,60 +3061,31 @@ def neighbours(self) -> list[BaseVertex]:
>>> v3 = g.add_vertex(coord=[2,2], name='v3')
>>> g.add_edge(v1, v2)
>>> g.add_edge(v1, v3)
>>> print(v1.neighbours())
>>> print(v1.adjacent())

:seealso: :meth:`neighbors` :meth:`incidences`
:seealso: :meth:`isadjacent` :meth:`incidences` :meth:`edges`
:meth:`_BaseGraph.adjacency`
"""
return [e.next(self) for e in self._edgelist]

def neighbors(self) -> list[BaseVertex]:
"""
Neighbors of a vertex

``v.neighbors()`` is a list of neighbors of this vertex.

.. note:: For a directed graph the neighbours are those on edges leaving this vertex

.. runblock:: pycon

>>> from pgraph import UGraph
>>> g = UGraph()
>>> v1 = g.add_vertex(coord=[0,0], name='v1')
>>> v2 = g.add_vertex(coord=[1,1], name='v2')
>>> g.add_edge(v1, v2)
>>> print(v1.neighbors())
#: alias of :meth:`adjacent`
neighbours = adjacent
#: alias of :meth:`adjacent`
neighbors = adjacent

:seealso: :meth:`neighbours`
def isadjacent(self, vertex: BaseVertex) -> bool:
"""
return [e.next(self) for e in self._edgelist]

def adjacent(self) -> list[BaseVertex]:
"""
Neighbours of a vertex (deprecated)

:return: a list of neighbours of this vertex
:rtype: list of BaseVertex subclass

.. deprecated:: use :meth:`neighbours` instead
"""
warnings.warn(
"adjacent() is deprecated, use neighbours() instead",
DeprecationWarning,
stacklevel=2,
)
return self.neighbours()

def isneighbour(self, vertex: BaseVertex) -> bool:
"""
Test if vertex is a neigbour
Test if a vertex is adjacent to this vertex

:param vertex: vertex reference
:type vertex: BaseVertex subclass
:return: true if a neighbour
:return: true if ``vertex`` is adjacent
:rtype: bool

For a directed graph this is true only if the edge is from ``self`` to
``vertex``.
For a directed graph this is true only if there is an edge from
``self`` to ``vertex``.

``isneighbour`` and ``isneighbor`` are aliases.

.. runblock:: pycon

Expand All @@ -3113,19 +3095,24 @@ def isneighbour(self, vertex: BaseVertex) -> bool:
>>> v2 = g.add_vertex(coord=[1,1], name='v2')
>>> v3 = g.add_vertex(coord=[2,2], name='v3')
>>> g.add_edge(v1, v2)
>>> print(v1.isneighbour(v2))
>>> print(v1.isneighbour(v3))
>>> print(v1.isadjacent(v2))
>>> print(v1.isadjacent(v3))

:seealso: :meth:`neighbours`
:seealso: :meth:`adjacent`
"""
return vertex in [e.next(self) for e in self._edgelist]

#: alias of :meth:`isadjacent`
isneighbour = isadjacent
#: alias of :meth:`isadjacent`
isneighbor = isadjacent

def incidences(self) -> list[tuple[BaseVertex, Edge]]:
"""
Neighbours and edges of a vertex

``v.incidences()`` is a generator that returns a list of incidences,
tuples of (vertex, edge) for all neighbours of the vertex ``v``.
tuples of (vertex, edge) for all vertices adjacent to the vertex ``v``.

.. note:: For a directed graph the edges are those leaving this vertex

Expand All @@ -3138,7 +3125,7 @@ def incidences(self) -> list[tuple[BaseVertex, Edge]]:
>>> g.add_edge(v1, v2)
>>> print(v1.incidences())

:seealso: :meth:`neighbours` :meth:`edges`
:seealso: :meth:`adjacent` :meth:`edges`
"""
return [(e.next(self), e) for e in self._edgelist]

Expand Down
108 changes: 108 additions & 0 deletions tests/test_adjacent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
``adjacent()`` is the canonical name for the vertices at the far end of a
vertex's edges, because "adjacent" is the graph-theory term and has one
spelling. ``neighbours`` and ``neighbors`` (and ``isneighbour`` /
``isneighbor``) remain as silent aliases: they are the same function, not a
second implementation, and they do not warn.
"""

import unittest
import warnings

from pgraph import DGraph, UGraph, DVertex, UVertex
from pgraph.PGraph import BaseVertex


class TestAliases(unittest.TestCase):
def test_aliases_are_the_same_function(self):
for cls in (BaseVertex, UVertex, DVertex):
with self.subTest(cls=cls.__name__):
self.assertIs(cls.neighbours, cls.adjacent)
self.assertIs(cls.neighbors, cls.adjacent)
self.assertIs(cls.isneighbour, cls.isadjacent)
self.assertIs(cls.isneighbor, cls.isadjacent)

def test_no_warnings(self):
g = UGraph()
a, b = g.add_vertex(name="a"), g.add_vertex(name="b")
a.connect(b)
with warnings.catch_warnings():
warnings.simplefilter("error") # any warning is an error
for name in ("adjacent", "neighbours", "neighbors"):
self.assertEqual(getattr(a, name)(), [b])
for name in ("isadjacent", "isneighbour", "isneighbor"):
self.assertTrue(getattr(a, name)(b))

def test_docstring_names_the_aliases(self):
doc = BaseVertex.adjacent.__doc__
for name in ("neighbours", "neighbors"):
self.assertIn(name, doc)


class _Adjacent:
Graph: type

def setUp(self):
self.g = self.Graph()
self.a = self.g.add_vertex([0, 0], name="a")
self.b = self.g.add_vertex([1, 0], name="b")
self.c = self.g.add_vertex([2, 0], name="c")

def test_simple(self):
self.a.connect(self.b)
self.assertEqual(self.a.adjacent(), [self.b])
self.assertTrue(self.a.isadjacent(self.b))
self.assertFalse(self.a.isadjacent(self.c))

def test_returns_a_new_list(self):
self.a.connect(self.b)
r = self.a.adjacent()
r.clear()
self.assertEqual(self.a.adjacent(), [self.b])

def test_parallel_edges_listed_once_per_edge(self):
for _ in range(3):
self.a.connect(self.b)
self.assertEqual(self.a.adjacent(), [self.b, self.b, self.b])

def test_matches_a_row_of_the_adjacency_matrix(self):
# the point of the name: row i of adjacency() is the vertices adjacent
# to vertex i, with multiplicity
self.a.connect(self.b)
self.a.connect(self.b)
self.a.connect(self.c)
A = self.g.adjacency()
for i, v in enumerate(self.g):
counts = {w: v.adjacent().count(w) for w in self.g}
for j, w in enumerate(self.g):
self.assertEqual(A[i, j], counts[w], (v.name, w.name))


class TestUGraphAdjacent(_Adjacent, unittest.TestCase):
Graph = UGraph

def test_both_ends(self):
self.a.connect(self.b)
self.assertEqual(self.b.adjacent(), [self.a])
self.assertTrue(self.b.isadjacent(self.a))

def test_self_loop_listed_twice(self):
self.a.connect(self.a)
self.assertEqual(self.a.adjacent(), [self.a, self.a])


class TestDGraphAdjacent(_Adjacent, unittest.TestCase):
Graph = DGraph

def test_outgoing_only(self):
self.a.connect(self.b)
self.assertEqual(self.b.adjacent(), [])
self.assertFalse(self.b.isadjacent(self.a))

def test_self_loop_listed_once(self):
self.a.connect(self.a)
self.assertEqual(self.a.adjacent(), [self.a])


if __name__ == "__main__":
unittest.main()
12 changes: 0 additions & 12 deletions tests/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,6 @@ def test_neighbours(self):
self.assertFalse(v1.isneighbour(v3))
self.assertFalse(v3.isneighbour(v1))

def test_adjacent_deprecated(self):
# adjacent() must still work (same result as neighbours()) but
# emit a DeprecationWarning, not a hard failure
g = UGraph()
v1 = g.add_vertex(name='v1')
v2 = g.add_vertex(name='v2')
v1.connect(v2)

with self.assertWarns(DeprecationWarning):
n = v1.adjacent()
self.assertEqual(n, v1.neighbours())

def test_getitem(self):
g = UGraph()
v1 = g.add_vertex(name='v1')
Expand Down
Loading