Optimized connectivity: Simultaneous face_edges and edge_nodes - #1560
Conversation
|
I think all the hard parts of the merge are done. At this point, the only failures seems to be sorting issues. |
@cmdupuis3 Thanks for your work, I will review it as soon as possible. And do you have any idea why the CIs are all failing? |
|
The CI is failing because the new algorithm returns the results in a different order. I was thinking we can add some sorting mechanism, at least for legacy behavior. Phillip was evidently aware of this issue too. It depends on if you need to support that... Personally I'd be okay with just changing it, but I think you would be a better judge of the situation. |
The optimized edge builder deduped half edges with a numba hash map, which
numbered edges in first-encounter order. Edges had previously been numbered
lexicographically by their (min_node, max_node) pair, as a side effect of the
np.unique(..., axis=0) the hash map replaced.
Global edge index is a public identity: it indexes edge_lon/edge_lat, edge
centered data variables, and edge_node_distances, so renumbering silently
re-pairs user data with different physical edges. It also broke the five
TestQuadHexagon connectivity tests, which assert on edge_node, face_edge,
node_edge, edge_face and face_face -- all the same renumbering cascading
through the derived connectivities.
Sort as the dedup mechanism instead of hashing. Node indices are dense
integers in [0, n_node), so a counting sort buckets the half edges by their
first node without any comparisons, and sorting each bucket by its second node
leaves the duplicates adjacent -- the dedup then falls out of the same walk.
Buckets hold one entry per edge incident to a node, so on a real mesh they are
tiny (node degree, typically under ten) and an insertion sort finishes them.
A bucket above MAX_INSERTION_SORT_SIZE is heap sorted so that a degenerate
mesh cannot degrade the build quadratically; np.argsort is deliberately not
used there, as numba's implementation degrades badly on structured input.
Half edges are identified throughout by their flat face_node_connectivity
index, which is also the face_edge_connectivity slot they are written back to,
so the sort needs a single permutation array and no mapping back.
This is faster and leaner than the hash map it replaces. On a synthetic one
million face quad mesh, measured by peak RSS rather than tracemalloc, which
does not observe numba's typed dict allocations:
dict build 397.3 ms 239.5 MB
bucket sort 85.1 ms 91.6 MB
The five legacy tests now pass unchanged. Adds order invariant coverage for
the canonical ordering and the face_edge positional contract, plus high degree
nodes either side of the insertion sort threshold.
Also casts n_nodes_per_face back to INT_DTYPE, so that the builder is not
compiled a second time for int64, and restores a blank line dropped between
two top level functions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
for more information, see https://pre-commit.ci
erogluorhan
left a comment
There was a problem hiding this comment.
Please see below a few comments:
Sevans711
left a comment
There was a problem hiding this comment.
All of my prior comments have been resolved and no glaring issues popped out at me from a final read-through, so I'm happy to approve!
|
Going through this in pieces, will have a few more comments later today. Good PR overall and happy to be reviewing it. |
| assert actual == expected | ||
|
|
||
| # Remaining slots stay padded | ||
| assert np.all(face_edges[face_idx, n_edges:] == INT_FILL_VALUE) |
There was a problem hiding this comment.
@cmdupuis3 The new ValueError in _populate_edge_node_connectivity can't be reached from any caller, so it's untested and shows as uncovered. This MPAS mesh already loads with edge_node_connectivity in _ds, so it pins the guard without a new fixture.
| assert np.all(face_edges[face_idx, n_edges:] == INT_FILL_VALUE) | |
| assert np.all(face_edges[face_idx, n_edges:] == INT_FILL_VALUE) | |
| def test_connectivity_edge_node_refuses_to_overwrite(gridpath): | |
| """Test that rebuilding edges on a grid that already has them is refused.""" | |
| from uxarray.grid.connectivity import _populate_edge_node_connectivity | |
| # This mesh supplies verticesOnEdge, so the grid loads with edges already present | |
| uxgrid = ux.open_grid(gridpath("mpas", "QU", "mesh.QU.1920km.151026.nc")) | |
| assert "edge_node_connectivity" in uxgrid._ds | |
| with pytest.raises(ValueError, match="already has"): | |
| _populate_edge_node_connectivity(uxgrid) |
|
Take outCSne30.ug. Renumber its face_edge_connectivity edge ids, put on a fresh grid with
Two smaller things in utils.py:529-536. The comment on MIN_ADAPTIVE_SORT_SIZE = 16 says a And test_connectivity_bucket_sort says the 500 bucket falls back to the heap sort, which it |
|
@rajeeja Wait, so if I understand correctly, this issue is more that the guards at call sites don't check both connectivities, so without having I'll see about consolidating the two sorting parameters into one and moving it to |
_populate_edge_node_connectivity writes both edge_node_connectivity and face_edge_connectivity, and numbers both in the constructed (lexicographic) edge order. The guard only covered edge_node_connectivity, so a grid holding a file-order face_edge_connectivity and no edge_node_connectivity passed every caller's check and had the stored variable silently renumbered on the first access to n_edge or edge_node_connectivity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
545000d to
c1fae0e
Compare
|
Yes, that covers both writes now, so the n_edge touch fails loud. |
|
@rajeeja As per a convo yesterday, @erogluorhan and I think the edge_node indexing situation is still a bit confusing, so I want to rethink it slightly before we commit to merging. |
Would close #1138, #1196
Related to #1180
Supercedes #1195
Overview
This set of changes optimizes face_edge, edge_node, and face_face connectivity. face_edge and edge_node connectivity are combined into one routine, while face_face is optimized stand-alone.
PR Checklist
General
Testing
Documentation
_) and have been added todocs/internal_api/index.rst