Skip to content

Commit 063d0ed

Browse files
committed
routing: improve feature pairing and terminal connections.
1 parent 8cbc432 commit 063d0ed

1 file changed

Lines changed: 47 additions & 12 deletions

File tree

‎mapmaker/routing/__init__.py‎

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,7 +1203,7 @@ def get_node_feature(node_dict, neighbour_features, used_features) -> Feature:
12031203
if len([node for node in subgraph.nodes if connectivity_graph.degree[node] == 1]) == 0:
12041204
pseudo_terminals += list(subgraph.nodes)[0:1]
12051205

1206-
# sorting nodes with priority -> terminal, number of features (2 than 1, than any size), distance to neighbours
1206+
# sorting nodes with degree-based heuristics
12071207
one_feature_terminals = {
12081208
n: min([
12091209
features[0].geometry.centroid.distance(nf.geometry.centroid)
@@ -1284,34 +1284,69 @@ def add_paths_to_neighbours(node, node_dict):
12841284
if connectivity_graph.degree(node) == 1
12851285
else [get_node_feature(node_dict, neighbour_features, used_features)]
12861286
)
1287+
1288+
# Precompute optimal feature-feature pairing when both sides have
1289+
# multiple features — ensures globally minimal total distance
1290+
# rather than greedy sequential assignment
1291+
_optimal_feature_pair = {}
1292+
if (neighbour_dict['type'] == 'feature'
1293+
and len(neighbour_dict.get('used', set())) == 0
1294+
and len(node_features) > 1
1295+
and len(neighbour_dict.get('features', [])) > 1):
1296+
a_feats = sorted(node_features, key=lambda f: f.id)
1297+
b_feats = sorted(neighbour_dict['features'], key=lambda f: f.id)
1298+
if len(a_feats) == len(b_feats):
1299+
min_cost = math.inf
1300+
best = None
1301+
for perm in itertools.permutations(range(len(b_feats))):
1302+
cost = sum(
1303+
b_feats[perm[i]].geometry.centroid.distance(
1304+
a_feats[i].geometry.centroid
1305+
)
1306+
for i in range(len(a_feats))
1307+
)
1308+
if cost < min_cost:
1309+
min_cost = cost
1310+
best = dict(zip(
1311+
[a.id for a in a_feats],
1312+
[b_feats[p] for p in perm]
1313+
))
1314+
_optimal_feature_pair = best or {}
1315+
12871316
for node_feature in node_features:
12881317
used_features.setdefault(node, set()).add(node_feature)
12891318
node_feature_centre = node_feature.geometry.centroid
12901319
debug_properties = connectivity_graph.get_edge_data(node, neighbour) or {}
12911320
if neighbour_dict['type'] == 'feature':
12921321
terminal_graph.add_node(node_feature.id, feature=node_feature)
12931322
if len(used_ids := neighbour_dict.get('used', set())):
1294-
closest_feature_id = self.__closest_feature_id_to_point(node_feature_centre, used_ids)
1295-
terminal_graph.add_edge(node_feature.id, closest_feature_id,
1296-
upstream=True, **debug_properties)
1323+
# Connect to ALL used features so that a terminal isn't limited
1324+
# to just one — the route graph can choose the correct path
1325+
for fid in used_ids:
1326+
terminal_graph.add_edge(node_feature.id, fid,
1327+
upstream=True, **debug_properties)
12971328
segments = set()
1298-
for connected_edges in route_graph[closest_feature_id].values():
1299-
for edge_dict in connected_edges.values():
1300-
if (segment_id := edge_dict.get('segment')) is not None:
1301-
segments.add(segment_id)
1302-
terminal_graph.nodes[closest_feature_id]['upstream'] = True
1303-
terminal_graph.nodes[closest_feature_id]['segments'] = segments
1329+
for fid in used_ids:
1330+
for connected_edges in route_graph[fid].values():
1331+
for edge_dict in connected_edges.values():
1332+
if (segment_id := edge_dict.get('segment')) is not None:
1333+
segments.add(segment_id)
1334+
for fid in used_ids:
1335+
terminal_graph.nodes[fid]['upstream'] = True
1336+
terminal_graph.nodes[fid]['segments'] = segments
13041337
else:
13051338
neighbour_terminal_laterals = [
13061339
k for k in connectivity_graph[neighbour]
13071340
if connectivity_graph.degree(k) == 1 and len(connectivity_graph.nodes[k].get('features', [])) == 2
13081341
]
13091342
neighbour_features = (
13101343
neighbour_dict.get('features', [])
1311-
if len(neighbour_dict.get('features', [])) <= 2 and degree == 1 and len(node_features) == 1
1344+
if len(neighbour_dict.get('features', [])) <= 2 and len(node_features) == 1
13121345
else [get_node_feature(neighbour_dict, [node_feature], used_features)]
13131346
if len(neighbour_terminal_laterals) > 0 and len(used_features.get(neighbour, set())) == 0
1314-
else [get_node_feature(neighbour_dict, [node_feature], used_features)]
1347+
else ([_optimal_feature_pair[node_feature.id]]
1348+
if node_feature.id in _optimal_feature_pair
1349+
else [get_node_feature(neighbour_dict, [node_feature], used_features)])
13151350
)
13161351
for neighbour_feature in neighbour_features:
13171352
used_features.setdefault(neighbour, set()).add(neighbour_feature)

0 commit comments

Comments
 (0)