Skip to content

Commit 419f7f9

Browse files
authored
Merge pull request #217 from NREL/ndr/misc-updates
Ndr/misc updates
2 parents 7ad6822 + b35158c commit 419f7f9

File tree

17 files changed

+832
-304
lines changed

17 files changed

+832
-304
lines changed

.pre-commit-config.yaml

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
repos:
2-
- repo: https://github.com/charliermarsh/ruff-pre-commit
3-
rev: v0.8.0
4-
hooks:
5-
- id: ruff
6-
- id: ruff-format
7-
8-
- repo: https://github.com/pre-commit/mirrors-mypy
9-
rev: "v1.13.0"
10-
hooks:
11-
- id: mypy
12-
additional_dependencies:
13-
[matplotlib, pandas-stubs, pytest, types-requests]
2+
- repo: local
3+
hooks:
4+
- id: pixi-check
5+
name: Run pixi checks
6+
entry: pixi run -e dev check
7+
language: system
8+
pass_filenames: false
9+
always_run: true

CLAUDE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Project Context
2+
3+
When working with this codebase, prioritize readability over cleverness. Ask clarifying questions before making architectural changes.
4+
5+
## Project overview
6+
7+
Mappymatch is a python package used to match a series of GPS waypoints (Trace) to a road network.
8+
9+
10+
## Common Commands
11+
12+
### running full check (test, types, lint, format)
13+
14+
```
15+
pixi run -e dev check
16+
```
17+

mappymatch/maps/nx/nx_map.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def from_geofence(
209209
network_type: NetworkType = NetworkType.DRIVE,
210210
custom_filter: Optional[str] = None,
211211
additional_metadata_keys: Optional[set | list] = None,
212+
filter_to_largest_component: bool = True,
212213
) -> NxMap:
213214
"""
214215
Read an OSM network graph into a NxMap
@@ -218,7 +219,9 @@ def from_geofence(
218219
xy: whether to use xy coordinates or lat/lon
219220
network_type: the network type to use for the graph
220221
custom_filter: a custom filter to pass to osmnx like '["highway"~"motorway|primary"]'
221-
additional_metadata_keys: additional keys to preserve in road metadata like '["maxspeed", "highway"]
222+
additional_metadata_keys: additional keys to preserve in road metadata like '["maxspeed", "highway"]'
223+
filter_to_largest_component: if True, keep only the largest strongly connected component;
224+
if False, keep all components (may result in routing failures between disconnected components)
222225
223226
Returns:
224227
a NxMap
@@ -237,6 +240,7 @@ def from_geofence(
237240
xy=xy,
238241
custom_filter=custom_filter,
239242
additional_metadata_keys=additional_metadata_keys,
243+
filter_to_largest_component=filter_to_largest_component,
240244
)
241245

242246
return NxMap(nx_graph)
@@ -374,12 +378,17 @@ def shortest_path(
374378
else:
375379
dest_id = dest_road.road_id.end
376380

377-
nx_route = nx.shortest_path(
378-
self.g,
379-
origin_id,
380-
dest_id,
381-
weight=weight,
382-
)
381+
try:
382+
nx_route = nx.shortest_path(
383+
self.g,
384+
origin_id,
385+
dest_id,
386+
weight=weight,
387+
)
388+
except nx.NetworkXNoPath:
389+
# No path exists between origin and destination
390+
# This can happen when the graph has multiple disconnected components
391+
return []
383392

384393
path = []
385394
for i in range(1, len(nx_route)):

mappymatch/maps/nx/readers/osm_readers.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def nx_graph_from_osmnx(
3838
xy: bool = True,
3939
custom_filter: Optional[str] = None,
4040
additional_metadata_keys: Optional[set] = None,
41+
filter_to_largest_component: bool = True,
4142
) -> nx.MultiDiGraph:
4243
"""
4344
Build a networkx graph from OSM data
@@ -48,6 +49,8 @@ def nx_graph_from_osmnx(
4849
xy: whether to use xy coordinates or lat/lon
4950
custom_filter: a custom filter to pass to osmnx
5051
additional_metadata_keys: additional keys to preserve in metadata
52+
filter_to_largest_component: if True, keep only the largest strongly connected component;
53+
if False, keep all components (may result in routing failures between disconnected components)
5154
5255
Returns:
5356
a networkx graph of the OSM network
@@ -68,6 +71,7 @@ def nx_graph_from_osmnx(
6871
network_type,
6972
xy=xy,
7073
additional_metadata_keys=additional_metadata_keys,
74+
filter_to_largest_component=filter_to_largest_component,
7175
)
7276

7377

@@ -76,6 +80,7 @@ def parse_osmnx_graph(
7680
network_type: NetworkType,
7781
xy: bool = True,
7882
additional_metadata_keys: Optional[set] = None,
83+
filter_to_largest_component: bool = True,
7984
) -> nx.MultiDiGraph:
8085
"""
8186
Parse the raw osmnx graph into a graph that we can use with our NxMap
@@ -85,6 +90,8 @@ def parse_osmnx_graph(
8590
xy: whether to use xy coordinates or lat/lon
8691
network_type: the network type to use for the graph
8792
additional_metadata_keys: additional keys to preserve in metadata
93+
filter_to_largest_component: if True, keep only the largest strongly connected component;
94+
if False, keep all components (may result in routing failures between disconnected components)
8895
8996
Returns:
9097
a cleaned networkx graph of the OSM network
@@ -107,15 +114,16 @@ def parse_osmnx_graph(
107114
nx.set_edge_attributes(g, kilometers, "kilometers")
108115

109116
# this makes sure there are no graph 'dead-ends'
110-
sg_components = nx.strongly_connected_components(g)
117+
if filter_to_largest_component:
118+
sg_components = nx.strongly_connected_components(g)
111119

112-
if not sg_components:
113-
raise MapException(
114-
"road network has no strongly connected components and is not routable; "
115-
"check polygon boundaries."
116-
)
120+
if not sg_components:
121+
raise MapException(
122+
"road network has no strongly connected components and is not routable; "
123+
"check polygon boundaries."
124+
)
117125

118-
g = nx.MultiDiGraph(g.subgraph(max(sg_components, key=len)))
126+
g = nx.MultiDiGraph(g.subgraph(max(sg_components, key=len)))
119127

120128
for u, v, d in g.edges(data=True):
121129
if "geometry" not in d:

mappymatch/matchers/lcss/constructs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def score_and_match(
115115
if m < 1:
116116
# todo: find a better way to handle this edge case
117117
raise Exception("traces of 0 points can't be matched")
118-
elif n < 2:
118+
elif n == 0:
119119
# a path was not found for this segment; might not be matchable;
120120
# we set a score of zero and return a set of no-matches
121121
matches = [

mappymatch/matchers/lcss/lcss.py

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import functools as ft
22
import logging
33

4-
from shapely.geometry import Point
5-
6-
from mappymatch.constructs.coordinate import Coordinate
74
from mappymatch.maps.map_interface import MapInterface
85
from mappymatch.matchers.lcss.constructs import TrajectorySegment
96
from mappymatch.matchers.lcss.ops import (
107
add_matches_for_stationary_points,
118
drop_stationary_points,
129
find_stationary_points,
10+
join_segment,
1311
new_path,
1412
same_trajectory_scheme,
1513
split_trajectory_segment,
@@ -59,31 +57,6 @@ def __init__(
5957
self.distance_threshold = distance_threshold
6058

6159
def match_trace(self, trace: Trace) -> MatchResult:
62-
def _join_segment(a: TrajectorySegment, b: TrajectorySegment):
63-
new_traces = a.trace + b.trace
64-
new_path = a.path + b.path
65-
66-
# test to see if there is a gap between the paths and if so,
67-
# try to connect it
68-
if len(a.path) > 1 and len(b.path) > 1:
69-
end_road = a.path[-1]
70-
start_road = b.path[0]
71-
if end_road.road_id.end != start_road.road_id.start:
72-
o = Coordinate(
73-
coordinate_id=None,
74-
geom=Point(end_road.geom.coords[-1]),
75-
crs=new_traces.crs,
76-
)
77-
d = Coordinate(
78-
coordinate_id=None,
79-
geom=Point(start_road.geom.coords[0]),
80-
crs=new_traces.crs,
81-
)
82-
path = self.road_map.shortest_path(o, d)
83-
new_path = a.path + path + b.path
84-
85-
return TrajectorySegment(new_traces, new_path)
86-
8760
stationary_index = find_stationary_points(trace)
8861

8962
sub_trace = drop_stationary_points(trace, stationary_index)
@@ -115,7 +88,7 @@ def _join_segment(a: TrajectorySegment, b: TrajectorySegment):
11588
# split and check the score
11689
new_split = split_trajectory_segment(road_map, scored_segment)
11790
joined_segment = ft.reduce(
118-
_join_segment, new_split
91+
lambda a, b: join_segment(road_map, a, b), new_split
11992
).score_and_match(de, dt)
12093
if joined_segment.score > scored_segment.score:
12194
# we found a better fit
@@ -128,7 +101,9 @@ def _join_segment(a: TrajectorySegment, b: TrajectorySegment):
128101

129102
scheme = next_scheme
130103

131-
joined_segment = ft.reduce(_join_segment, scheme).score_and_match(de, dt)
104+
joined_segment = ft.reduce(
105+
lambda a, b: join_segment(road_map, a, b), scheme
106+
).score_and_match(de, dt)
132107

133108
matches = joined_segment.matches
134109

mappymatch/matchers/lcss/ops.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from copy import deepcopy
33
from typing import Any, List, NamedTuple
44

5+
from shapely.geometry import Point
6+
57
from mappymatch.constructs.coordinate import Coordinate
68
from mappymatch.constructs.match import Match
79
from mappymatch.constructs.road import Road
@@ -16,6 +18,49 @@
1618
log = logging.getLogger(__name__)
1719

1820

21+
def join_segment(
22+
road_map: MapInterface, a: TrajectorySegment, b: TrajectorySegment
23+
) -> TrajectorySegment:
24+
"""
25+
Join two trajectory segments together, attempting to route between them if needed.
26+
27+
Args:
28+
road_map: The road map to use for routing
29+
a: The first trajectory segment
30+
b: The second trajectory segment
31+
32+
Returns:
33+
A new trajectory segment combining both segments
34+
"""
35+
new_traces = a.trace + b.trace
36+
new_path = a.path + b.path
37+
38+
# test to see if there is a gap between the paths and if so,
39+
# try to connect it
40+
if len(a.path) > 0 and len(b.path) > 0:
41+
end_road = a.path[-1]
42+
start_road = b.path[0]
43+
if end_road.road_id.end != start_road.road_id.start:
44+
o = Coordinate(
45+
coordinate_id=None,
46+
geom=Point(end_road.geom.coords[-1]),
47+
crs=new_traces.crs,
48+
)
49+
d = Coordinate(
50+
coordinate_id=None,
51+
geom=Point(start_road.geom.coords[0]),
52+
crs=new_traces.crs,
53+
)
54+
path = road_map.shortest_path(o, d)
55+
# If no path exists (disconnected components), just concatenate the paths
56+
if path:
57+
new_path = a.path + path + b.path
58+
else:
59+
new_path = a.path + b.path
60+
61+
return TrajectorySegment(new_traces, new_path)
62+
63+
1964
def new_path(
2065
road_map: MapInterface,
2166
trace: Trace,

0 commit comments

Comments
 (0)