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
64 changes: 58 additions & 6 deletions src/pgraph/PGraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -3902,6 +3902,7 @@ def incidences(self) -> list[tuple[Vertex, Edge]]:

:seealso: :meth:`adjacent` :meth:`isadjacent` :meth:`successors`
:meth:`out_edges` :meth:`in_edges` :meth:`predecessors`
:meth:`edgeto` :meth:`edges_to`
:meth:`edges` :meth:`edgeto` :attr:`degree`
:meth:`Graph.adjacency`
"""
Expand Down Expand Up @@ -4016,11 +4017,11 @@ def _bind_edge(self, edge: Edge, dest: Vertex) -> None:

def edgeto(self, dest: Vertex) -> Edge:
"""
Get edge connecting vertex to specific neighbour
The edge from this vertex to an adjacent vertex

:param dest: a neigbouring vertex
:param dest: an adjacent vertex
:type dest: ``Vertex`` subclass
:raises ValueError: ``dest`` is not a neighbour
:raises ValueError: ``dest`` is not adjacent to this vertex
:return: the edge from this vertex to ``dest``
:rtype: Edge

Expand All @@ -4038,15 +4039,65 @@ def edgeto(self, dest: Vertex) -> Edge:
>>> v2 = g.add_vertex(coord=[1,1], name='v2')
>>> g.add_edge(v1, v2, cost=1.414)
>>> print(v1.edgeto(v2))

:seealso: :meth:`edges_to` :meth:`incidences` :meth:`adjacent`
"""
edges = [e for n, e in self.incidences() if n is dest]
edges = self.edges_to(dest)
if not edges:
raise ValueError("dest is not a neighbour")
costed = [e for e in edges if e.cost is not None]
if costed:
return min(costed, key=lambda e: e.cost) # type: ignore[arg-type,return-value]
return edges[0]

def edges_to(self, dest: Vertex) -> list[Edge]:
"""
All the edges from this vertex to another vertex

:param dest: the vertex at the far end
:type dest: ``Vertex`` subclass
:return: every edge from this vertex to ``dest``, in the order they
were connected, which is empty if there are none
:rtype: list of Edge

This is how to see parallel edges. :meth:`edgeto` returns the one
edge that a path would use, the cheapest, whereas this returns them
all.

.. note::

- For a directed graph only the edges leaving this vertex for
``dest`` are included, so ``a.edges_to(b)`` and
``b.edges_to(a)`` are different lists
- For an undirected graph the edges are found from either end,
whichever vertex they were connected from
- A self-loop is listed once, although its vertex holds it twice
(see :meth:`edges`)
- Unlike :meth:`edgeto`, there being no edge is not an error, the
list is empty
- The list is a new one, so changing it does not change the graph

.. 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, cost=3)
>>> g.add_edge(v1, v2, cost=1)
>>> print(v1.edges_to(v2))
>>> print(v1.edgeto(v2))

:seealso: :meth:`edgeto` :meth:`edges` :meth:`out_edges`
:meth:`incidences` :meth:`adjacent`
"""
found: list[Edge] = []
for vertex, edge in self.incidences():
# a self-loop is held twice by its vertex, so is met twice
if vertex is dest and not any(edge is e for e in found):
found.append(edge)
return found

def edges(self) -> list[Edge]:
"""
Return a list of the edges for vertex
Expand Down Expand Up @@ -4074,7 +4125,8 @@ def edges(self) -> list[Edge]:
>>> print(v1.edges())

:seealso: :meth:`out_edges` :meth:`in_edges` :meth:`adjacent`
:meth:`incidences` :attr:`degree` :meth:`Graph.edges`
:meth:`incidences` :meth:`edges_to` :attr:`degree`
:meth:`Graph.edges`
"""
return list(self._edgelist)

Expand Down Expand Up @@ -4226,7 +4278,7 @@ def out_edges(self) -> list[Edge]:
graph every incident edge is outgoing.

:seealso: :meth:`in_edges` :meth:`edges` :meth:`adjacent` :meth:`successors`
:meth:`incidences` :attr:`out_degree`
:meth:`incidences` :meth:`edges_to` :attr:`out_degree`
"""
return list(self._edgelist)

Expand Down
103 changes: 103 additions & 0 deletions tests/test_edges_to.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""``edges_to(dest)`` lists every edge from a vertex to ``dest``.

``edgeto(dest)`` returns the one edge a path would use, the cheapest of any
parallel edges. ``edges_to`` is the whole set, so that parallel edges can be
inspected: in connection order, empty rather than an error when there are none,
and with an undirected self-loop listed once, not once per end.
"""

import unittest

from pgraph import DGraph, UGraph


class _EdgesTo:
Graph: type

def setUp(self):
self.g = self.Graph()
self.a, self.b, self.c = (self.g.add_vertex(name=n) for n in "abc")

def test_single_edge(self):
e = self.a.connect(self.b, cost=3)
self.assertEqual(self.a.edges_to(self.b), [e])

def test_parallel_edges_in_connection_order(self):
e1 = self.a.connect(self.b, cost=5)
e2 = self.a.connect(self.b, cost=2)
e3 = self.a.connect(self.b, cost=9)
self.assertEqual(self.a.edges_to(self.b), [e1, e2, e3])

def test_edgeto_is_the_cheapest_of_edges_to(self):
edges = [self.a.connect(self.b, cost=c) for c in (5, 2, 9)]
self.assertIs(self.a.edgeto(self.b), edges[1])
self.assertIn(self.a.edgeto(self.b), self.a.edges_to(self.b))

def test_none_is_an_empty_list_not_an_error(self):
self.a.connect(self.b)
self.assertEqual(self.a.edges_to(self.c), [])
with self.assertRaises(ValueError):
self.a.edgeto(self.c) # edgeto has no edge to return

def test_other_destinations_are_not_included(self):
e = self.a.connect(self.b)
self.a.connect(self.c)
self.assertEqual(self.a.edges_to(self.b), [e])

def test_self_loop_is_listed_once(self):
loop = self.a.connect(self.a, cost=1)
self.assertEqual(self.a.edges_to(self.a), [loop])

def test_loop_and_parallel_loops(self):
l1 = self.a.connect(self.a)
l2 = self.a.connect(self.a)
self.assertEqual(self.a.edges_to(self.a), [l1, l2])

def test_is_a_new_list(self):
self.a.connect(self.b)
self.a.edges_to(self.b).clear()
self.assertEqual(len(self.a.edges_to(self.b)), 1)
self.assertEqual(self.g.validate(), [])

def test_removed_edge_is_gone(self):
e1 = self.a.connect(self.b)
e2 = self.a.connect(self.b)
self.g.remove_edge(e1)
self.assertEqual(self.a.edges_to(self.b), [e2])

def test_unconnected_vertex(self):
self.assertEqual(self.a.edges_to(self.b), [])


class TestUndirectedEdgesTo(_EdgesTo, unittest.TestCase):
Graph = UGraph

def test_found_from_either_end(self):
e = self.a.connect(self.b)
self.assertEqual(self.a.edges_to(self.b), [e])
self.assertEqual(self.b.edges_to(self.a), [e])

def test_edges_connected_from_the_other_end_are_included(self):
e1 = self.a.connect(self.b)
e2 = self.b.connect(self.a)
self.assertEqual(self.a.edges_to(self.b), [e1, e2])
self.assertEqual(self.b.edges_to(self.a), [e1, e2])


class TestDirectedEdgesTo(_EdgesTo, unittest.TestCase):
Graph = DGraph

def test_only_the_direction_given(self):
e = self.a.connect(self.b)
self.assertEqual(self.a.edges_to(self.b), [e])
self.assertEqual(self.b.edges_to(self.a), [])

def test_opposite_edges_are_separate(self):
ab = self.a.connect(self.b)
ba = self.b.connect(self.a)
self.assertEqual(self.a.edges_to(self.b), [ab])
self.assertEqual(self.b.edges_to(self.a), [ba])


if __name__ == "__main__":
unittest.main()
Loading