@@ -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