Skip to content

Commit

Permalink
Provide simple GraphLike and NodeLike implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
MKuranowski committed Jul 21, 2024
1 parent c74e8d6 commit 9fb06e1
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 102 deletions.
4 changes: 4 additions & 0 deletions pyroutelib3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
find_route,
find_route_without_turn_around,
)
from .simple_graph import SimpleExternalNode, SimpleGraph, SimpleNode

__all__ = [
"DEFAULT_STEP_LIMIT",
Expand All @@ -29,6 +30,9 @@
"haversine_earth_distance",
"osm",
"protocols",
"SimpleExternalNode",
"SimpleGraph",
"SimpleNode",
"StepLimitExceeded",
"taxicab_distance",
]
23 changes: 8 additions & 15 deletions pyroutelib3/osm/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from ..distance import haversine_earth_distance
from ..protocols import Position
from ..simple_graph import SimpleExternalNode, SimpleGraph
from . import reader
from .profile import Profile, TurnRestriction

Expand All @@ -35,25 +36,18 @@ def pairwise(iterable: Iterable[_T]) -> Iterable[Tuple[_T, _T]]:
from itertools import pairwise


@dataclass(frozen=True)
class GraphNode:
class GraphNode(SimpleExternalNode):
"""GraphNode is a *node* in a :py:class:`Graph`."""

id: int
position: Position
osm_id: int

@property
def external_id(self) -> int:
return self.osm_id
def osm_id(self) -> int:
return self.external_id


class Graph:
class Graph(SimpleGraph[GraphNode]):
"""Graph implements :py:class:`GraphLike` over OpenStreetMap data."""

profile: Profile
nodes: Dict[int, GraphNode]
edges: Dict[int, Dict[int, float]]

_phantom_node_id_counter: int
"""_phantom_node_id_counter is a counter used for generating IDs for phantom nodes
Expand All @@ -62,9 +56,8 @@ class Graph:
"""

def __init__(self, profile: Profile) -> None:
super().__init__()
self.profile = profile
self.nodes = {}
self.edges = {}
self._phantom_node_id_counter = _MAX_NODE_ID

def get_node(self, id: int) -> GraphNode:
Expand Down Expand Up @@ -189,7 +182,7 @@ def add_node(self, node: reader.Node) -> None:
self.g.nodes[node.id] = GraphNode(
id=node.id,
position=node.position,
osm_id=node.id,
external_id=node.id,
)
self.unused_nodes.add(node.id)

Expand Down Expand Up @@ -583,7 +576,7 @@ def _clone_nodes(self) -> None:
self.g.nodes[new_id] = GraphNode(
id=new_id,
position=old_node.position,
osm_id=old_node.osm_id,
external_id=old_node.osm_id,
)
self.g.edges[new_id] = self.g.edges[old_id].copy()

Expand Down
52 changes: 26 additions & 26 deletions pyroutelib3/osm/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_simple_graph(self) -> None:

# Check turn restriction -200: no -8 → -7 → -3
self.assertNoEdge(g, -8, -7)
phantom_nodes = [id for id in g.edges[-8] if g.nodes[id].osm_id == -7]
phantom_nodes = [id for id in g.edges[-8] if g.nodes[id].external_id == -7]
self.assertEqual(len(phantom_nodes), 1)
phantom_node = phantom_nodes[0]
self.assertEdge(g, -8, phantom_node)
Expand All @@ -79,7 +79,7 @@ def test_simple_graph(self) -> None:

# Check turn restriction: only -1 → -2 → -3
self.assertNoEdge(g, -1, -2)
phantom_nodes = [id for id in g.edges[-1] if g.nodes[id].osm_id == -2]
phantom_nodes = [id for id in g.edges[-1] if g.nodes[id].external_id == -2]
self.assertEqual(len(phantom_nodes), 1)
phantom_node = phantom_nodes[0]
self.assertEdge(g, -1, phantom_node)
Expand All @@ -92,16 +92,16 @@ def test_add_node(self) -> None:
b = _GraphBuilder(g)
b.add_node(reader.Node(1, (0.0, 0.0)))

self.assertEqual(g.nodes[1], GraphNode(id=1, position=(0.0, 0.0), osm_id=1))
self.assertEqual(g.nodes[1], GraphNode(id=1, position=(0.0, 0.0), external_id=1))
self.assertIn(1, b.unused_nodes)

def test_add_node_duplicate(self) -> None:
g = Graph(CarProfile())
g.nodes[1] = GraphNode(id=1, position=(0.0, 0.0), osm_id=1)
g.nodes[1] = GraphNode(id=1, position=(0.0, 0.0), external_id=1)
b = _GraphBuilder(g)
b.add_node(reader.Node(1, (0.1, 0.0)))

self.assertEqual(g.nodes[1], GraphNode(id=1, position=(0.0, 0.0), osm_id=1))
self.assertEqual(g.nodes[1], GraphNode(id=1, position=(0.0, 0.0), external_id=1))
self.assertNotIn(1, b.unused_nodes)

def test_add_node_big_osm_id(self) -> None:
Expand Down Expand Up @@ -759,11 +759,11 @@ def setUp(self) -> None:
# (100) (100)
self.g = Graph(CarProfile())
self.g.nodes = {
1: GraphNode(id=1, position=(0.0, 0.0), osm_id=1),
2: GraphNode(id=2, position=(0.1, 0.0), osm_id=2),
3: GraphNode(id=3, position=(0.2, 0.0), osm_id=3),
4: GraphNode(id=4, position=(0.3, 0.0), osm_id=4),
5: GraphNode(id=5, position=(0.2, 0.1), osm_id=5),
1: GraphNode(id=1, position=(0.0, 0.0), external_id=1),
2: GraphNode(id=2, position=(0.1, 0.0), external_id=2),
3: GraphNode(id=3, position=(0.2, 0.0), external_id=3),
4: GraphNode(id=4, position=(0.3, 0.0), external_id=4),
5: GraphNode(id=5, position=(0.2, 0.1), external_id=5),
}
self.g.edges = {
1: {2: 200.0},
Expand All @@ -785,7 +785,7 @@ def test_restriction_as_cloned_nodes(self) -> None:
self.assertEqual(change.phantom_node_id_counter, 11)

def test_restriction_as_cloned_nodes_reuses_inner_nodes(self) -> None:
self.g.nodes[11] = GraphNode(id=11, position=(0.1, 0.0), osm_id=2)
self.g.nodes[11] = GraphNode(id=11, position=(0.1, 0.0), external_id=2)
del self.g.edges[1][2]
self.g.edges[1][11] = 200.0
self.g.edges[11] = {1: 200.0, 3: 200.0, 5: 100.0}
Expand All @@ -798,8 +798,8 @@ def test_restriction_as_cloned_nodes_reuses_inner_nodes(self) -> None:
self.assertSetEqual(change.edges_to_remove, set())

def test_restriction_as_cloned_nodes_reuses_last_nodes(self) -> None:
self.g.nodes[11] = GraphNode(id=11, position=(0.1, 0.0), osm_id=2)
self.g.nodes[12] = GraphNode(id=12, position=(0.1, 0.0), osm_id=3)
self.g.nodes[11] = GraphNode(id=11, position=(0.1, 0.0), external_id=2)
self.g.nodes[12] = GraphNode(id=12, position=(0.1, 0.0), external_id=3)
del self.g.edges[1][2]
self.g.edges[1][11] = 200.0
self.g.edges[11] = {1: 200.0, 12: 200.0, 5: 100.0}
Expand Down Expand Up @@ -827,12 +827,12 @@ def test_apply(self) -> None:
self.assertDictEqual(
self.g.nodes,
{
1: GraphNode(id=1, position=(0.0, 0.0), osm_id=1),
2: GraphNode(id=2, position=(0.1, 0.0), osm_id=2),
3: GraphNode(id=3, position=(0.2, 0.0), osm_id=3),
4: GraphNode(id=4, position=(0.3, 0.0), osm_id=4),
5: GraphNode(id=5, position=(0.2, 0.1), osm_id=5),
11: GraphNode(id=11, position=(0.1, 0.0), osm_id=2),
1: GraphNode(id=1, position=(0.0, 0.0), external_id=1),
2: GraphNode(id=2, position=(0.1, 0.0), external_id=2),
3: GraphNode(id=3, position=(0.2, 0.0), external_id=3),
4: GraphNode(id=4, position=(0.3, 0.0), external_id=4),
5: GraphNode(id=5, position=(0.2, 0.1), external_id=5),
11: GraphNode(id=11, position=(0.1, 0.0), external_id=2),
},
)
self.assertDictEqual(
Expand All @@ -859,13 +859,13 @@ def test_ensure_only_edge(self) -> None:
self.assertDictEqual(
self.g.nodes,
{
1: GraphNode(id=1, position=(0.0, 0.0), osm_id=1),
2: GraphNode(id=2, position=(0.1, 0.0), osm_id=2),
3: GraphNode(id=3, position=(0.2, 0.0), osm_id=3),
4: GraphNode(id=4, position=(0.3, 0.0), osm_id=4),
5: GraphNode(id=5, position=(0.2, 0.1), osm_id=5),
11: GraphNode(id=11, position=(0.1, 0.0), osm_id=2),
12: GraphNode(id=12, position=(0.2, 0.0), osm_id=3),
1: GraphNode(id=1, position=(0.0, 0.0), external_id=1),
2: GraphNode(id=2, position=(0.1, 0.0), external_id=2),
3: GraphNode(id=3, position=(0.2, 0.0), external_id=3),
4: GraphNode(id=4, position=(0.3, 0.0), external_id=4),
5: GraphNode(id=5, position=(0.2, 0.1), external_id=5),
11: GraphNode(id=11, position=(0.1, 0.0), external_id=2),
12: GraphNode(id=12, position=(0.2, 0.0), external_id=3),
},
)
self.assertDictEqual(
Expand Down
48 changes: 48 additions & 0 deletions pyroutelib3/simple_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from dataclasses import dataclass, field
from typing import Dict, Iterable, Tuple

from typing_extensions import Self

from .protocols import GraphLike, NodeLikeT_co, Position


@dataclass
class SimpleNode:
"""SimpleNode provides a base class and a simple implementation of
the :py:class:`NodeLike` protocol."""

id: int
position: Position


@dataclass
class SimpleExternalNode:
"""SimpleExternalNode provides a base class and a simple implementation of
the :py:class:`ExternalNodeLike` protocol."""

id: int
position: Position
external_id: int

@classmethod
def with_same_external_id(cls, id: int, position: Position) -> Self:
"""with_same_external_id instantiates a SimpleExternalNode with
:py:attr:`external_id` set to the same value as :py:attr:`id`.
"""
return cls(id=id, position=position, external_id=id)


@dataclass
class SimpleGraph(GraphLike[NodeLikeT_co]):
"""SimpleGraph provides a base class and a simple implementation of
the :py:class:`GraphLike` protocol over two dictionaries: one holding nodes,
and another holding edge costs."""

nodes: Dict[int, NodeLikeT_co] = field(default_factory=dict)
edges: Dict[int, Dict[int, float]] = field(default_factory=dict)

def get_node(self, id: int) -> NodeLikeT_co:
return self.nodes[id]

def get_edges(self, id: int) -> Iterable[Tuple[int, float]]:
return self.edges[id].items()
97 changes: 36 additions & 61 deletions pyroutelib3/test_router.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,8 @@
from dataclasses import dataclass
from typing import Dict, Iterable, Tuple
from unittest import TestCase

from .distance import euclidean_distance
from .protocols import GraphLike, Position
from .router import StepLimitExceeded, find_route, find_route_without_turn_around


@dataclass
class Node:
id: int
position: Position
external_id: int = -1

def __post_init__(self) -> None:
if self.external_id < 0:
self.external_id = self.id


@dataclass
class Graph(GraphLike[Node]):
nodes: Dict[int, Node]
edges: Dict[int, Dict[int, float]]

def get_node(self, id: int) -> Node:
return self.nodes[id]

def get_edges(self, id: int) -> Iterable[Tuple[int, float]]:
return self.edges[id].items()
from .simple_graph import SimpleExternalNode, SimpleGraph, SimpleNode


class TestFindRoute(TestCase):
Expand All @@ -36,13 +11,13 @@ def test_simple(self) -> None:
# 1─────2─────3─────4
# └─────5─────┘
# (10) (10)
g = Graph(
g = SimpleGraph(
nodes={
1: Node(1, (1, 1)),
2: Node(2, (2, 1)),
3: Node(3, (3, 1)),
4: Node(4, (4, 1)),
5: Node(5, (3, 0)),
1: SimpleNode(1, (1, 1)),
2: SimpleNode(2, (2, 1)),
3: SimpleNode(3, (3, 1)),
4: SimpleNode(4, (4, 1)),
5: SimpleNode(5, (3, 0)),
},
edges={
1: {2: 20},
Expand All @@ -69,17 +44,17 @@ def test_shortest_not_optimal(self) -> None:
# │60 │50 │10
# │ 10 │ 20 │
# 1─────2─────3
g = Graph(
g = SimpleGraph(
nodes={
1: Node(1, (0, 0)),
2: Node(2, (1, 0)),
3: Node(3, (2, 0)),
4: Node(4, (0, 1)),
5: Node(5, (1, 1)),
6: Node(6, (2, 1)),
7: Node(7, (0, 2)),
8: Node(8, (1, 2)),
9: Node(9, (2, 2)),
1: SimpleNode(1, (0, 0)),
2: SimpleNode(2, (1, 0)),
3: SimpleNode(3, (2, 0)),
4: SimpleNode(4, (0, 1)),
5: SimpleNode(5, (1, 1)),
6: SimpleNode(6, (2, 1)),
7: SimpleNode(7, (0, 2)),
8: SimpleNode(8, (1, 2)),
9: SimpleNode(9, (2, 2)),
},
edges={
1: {2: 10, 4: 60},
Expand All @@ -104,13 +79,13 @@ def test_step_limit(self) -> None:
# 1─────2─────3─────4
# └─────5─────┘
# (10) (10)
g = Graph(
g = SimpleGraph(
nodes={
1: Node(1, (1, 1)),
2: Node(2, (2, 1)),
3: Node(3, (3, 1)),
4: Node(4, (4, 1)),
5: Node(5, (3, 0)),
1: SimpleNode(1, (1, 1)),
2: SimpleNode(2, (2, 1)),
3: SimpleNode(3, (3, 1)),
4: SimpleNode(4, (4, 1)),
5: SimpleNode(5, (3, 0)),
},
edges={
1: {2: 20},
Expand All @@ -137,14 +112,14 @@ def test(self) -> None:
# │ 10 │
# 3─────5
# mandatory 1-2-4
g = Graph(
g = SimpleGraph(
nodes={
1: Node(1, (0, 2)),
2: Node(2, (0, 1)),
20: Node(20, (0, 1), external_id=2),
3: Node(3, (0, 0)),
4: Node(4, (1, 1)),
5: Node(5, (1, 0)),
1: SimpleExternalNode.with_same_external_id(1, (0, 2)),
2: SimpleExternalNode.with_same_external_id(2, (0, 1)),
20: SimpleExternalNode(20, (0, 1), external_id=2),
3: SimpleExternalNode.with_same_external_id(3, (0, 0)),
4: SimpleExternalNode.with_same_external_id(4, (1, 1)),
5: SimpleExternalNode.with_same_external_id(5, (1, 0)),
},
edges={
1: {20: 10},
Expand All @@ -171,13 +146,13 @@ def test_step_limit(self) -> None:
# 1─────2─────3─────4
# └─────5─────┘
# (10) (10)
g = Graph(
g = SimpleGraph(
nodes={
1: Node(1, (1, 1)),
2: Node(2, (2, 1)),
3: Node(3, (3, 1)),
4: Node(4, (4, 1)),
5: Node(5, (3, 0)),
1: SimpleExternalNode.with_same_external_id(1, (1, 1)),
2: SimpleExternalNode.with_same_external_id(2, (2, 1)),
3: SimpleExternalNode.with_same_external_id(3, (3, 1)),
4: SimpleExternalNode.with_same_external_id(4, (4, 1)),
5: SimpleExternalNode.with_same_external_id(5, (3, 0)),
},
edges={
1: {2: 20},
Expand Down

0 comments on commit 9fb06e1

Please sign in to comment.