Description
Building on the benchmarks and flamegraph profiling introduced in #117, this issue targets the a bottleneck identified in connect_nodes_across_graphs :- the repeated per-node KDTree queries and edge insertions that could be replaced with vectorized, batched operations.
The current implementation in base.py has three compounding inefficiencies inside a Python loop over all N_target nodes:
- Serial KDTree queries :
_find_neighbour_node_idxs_in_source_mesh(xy_target) is called once per target node, passing a single 2D point. All three underlying methods (kdt_s.query() for nearest_neighbour/nearest_neighbours, kdt_s.query_ball_point() for within_radius) natively accept an array of points so N_target individual Python-level calls could become one.
- Per-edge
add_edge calls : each edge is inserted individually into the DiGraph. NetworkX supports add_edges_from() which takes a list, cutting down on per-call overhead.
- Per-edge attribute computation :
len (Euclidean distance) and vdiff (position difference) are computed as scalar numpy ops inside the inner loop, when they could be derived in a single vectorized pass over all edges once neighbour indices are known.
Also worth noting: containing_rectangle calls connect_nodes_across_graphs recursively with within_radius any changes here need to keep that path intact.
The flamegraph from #117 makes this visible. We can see connect_nodes_across_graphs dominates the call stack:

Proposed Enhancements
High-level direction:
- Stack all target node positions into a single array, run one batched KDTree query, build the full edge list from the result
nearest_neighbour / nearest_neighbours are the most straightforward, batch query returns a regular (N_target, k) array
within_radius needs a bit more thought since query_ball_point on an array returns a ragged list (variable neighbours per node), so edge-list construction is less clean. Open to ideas on the best approach here.
Happy to discuss before going to implementation.
Context & Links
Task List
Description
Building on the benchmarks and flamegraph profiling introduced in #117, this issue targets the a bottleneck identified in
connect_nodes_across_graphs:- the repeated per-node KDTree queries and edge insertions that could be replaced with vectorized, batched operations.The current implementation in
base.pyhas three compounding inefficiencies inside a Python loop over allN_targetnodes:_find_neighbour_node_idxs_in_source_mesh(xy_target)is called once per target node, passing a single 2D point. All three underlying methods (kdt_s.query()fornearest_neighbour/nearest_neighbours,kdt_s.query_ball_point()forwithin_radius) natively accept an array of points soN_targetindividual Python-level calls could become one.add_edgecalls : each edge is inserted individually into theDiGraph. NetworkX supportsadd_edges_from()which takes a list, cutting down on per-call overhead.len(Euclidean distance) andvdiff(position difference) are computed as scalar numpy ops inside the inner loop, when they could be derived in a single vectorized pass over all edges once neighbour indices are known.Also worth noting:
containing_rectanglecallsconnect_nodes_across_graphsrecursively withwithin_radiusany changes here need to keep that path intact.The flamegraph from #117 makes this visible. We can see
connect_nodes_across_graphsdominates the call stack:Proposed Enhancements
High-level direction:
nearest_neighbour/nearest_neighboursare the most straightforward, batch query returns a regular(N_target, k)arraywithin_radiusneeds a bit more thought sincequery_ball_pointon an array returns a ragged list (variable neighbours per node), so edge-list construction is less clean. Open to ideas on the best approach here.Happy to discuss before going to implementation.
Context & Links
src/weather_model_graphs/create/base.pyTask List
nearest_neighbourandnearest_neighbourswithin_radius&query_ball_pointragged outputadd_edgeloop with a singleadd_edges_fromcalllenandvdiffedge attribute computationcontaining_rectangle(recursive call) still works correctly