Coalesce and strip explicit zeros before CAN index access - #376
Conversation
LiftLayer and the CAN attention modules call indices() on user-supplied neighborhood matrices. Sparse matrices built with toponetx's from_sparse preserve explicit zero entries (e.g. zero-valued diagonal self-loops emitted by CellComplex.adjacency_matrix), so the number of stored entries does not match the number of true neighborhood relations: - uncoalesced input raised RuntimeError on indices()/values() - explicit zeros created phantom neighbors in attention softmax - LiftLayer produced one signal row per stored entry instead of per edge, mismatching x_1 when concatenating Add a sanitize_neighborhood helper that coalesces and removes explicit zero entries, and apply it before all index access. References #236, references #242
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #376 +/- ##
==========================================
- Coverage 96.43% 96.36% -0.08%
==========================================
Files 58 58
Lines 2078 2090 +12
==========================================
+ Hits 2004 2014 +10
- Misses 74 76 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR addresses correctness issues in CAN modules when consuming user-supplied sparse neighborhood matrices that may be uncoalesced and/or contain explicit stored zeros (e.g., produced by toponetx.utils.sparse.from_sparse), which previously led to runtime errors on indices()/values() and phantom neighborhood relations during attention computation.
Changes:
- Adds a
sanitize_neighborhoodhelper to coalesce sparse COO neighborhoods and drop explicit zero entries. - Applies neighborhood sanitization prior to index-based access in
LiftLayerand the CAN attention modules to align stored entries with true neighborhood relations.
Suppressed comments (1)
topomodelx/nn/cell/can_layer.py:771
- Same as above: once
sanitize_neighborhood()has stripped explicit zeros, checking emptiness via_nnz()avoids two full scans overvalues().nonzero()and keeps the early-return logic simpler.
neighborhood = sanitize_neighborhood(neighborhood)
# If there are no non-zero values in the neighborhood, then the neighborhood is empty. -> return zero tensor
if not neighborhood.values().nonzero().size(0) > 0 and self.concat:
return torch.zeros(
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| neighborhood = neighborhood.coalesce() | ||
| values = neighborhood.values() | ||
| keep = values != 0 | ||
| if bool(keep.all()): | ||
| return neighborhood | ||
| indices = neighborhood.indices()[:, keep] | ||
| return torch.sparse_coo_tensor( | ||
| indices, values[keep], neighborhood.shape, device=neighborhood.device | ||
| ).coalesce() |
| def sanitize_neighborhood(neighborhood: torch.Tensor) -> torch.Tensor: | ||
| r"""Return a coalesced neighborhood matrix without explicit zero entries. | ||
|
|
||
| The number of stored entries of the returned tensor equals its number of | ||
| true non-zero entries, so that iterating over stored entries (e.g. via | ||
| ``indices()``) corresponds to actual neighborhood relations. This matters | ||
| because sparse matrices built with ``toponetx.utils.sparse.from_sparse`` | ||
| preserve explicit zeros that other construction paths (e.g. dense casting) | ||
| drop. |
| neighborhood = sanitize_neighborhood(neighborhood) | ||
|
|
||
| # If there are no non-zero values in the neighborhood, then the neighborhood is empty. -> return zero tensor | ||
| if not neighborhood.values().nonzero().size(0) > 0 and self.concat: | ||
| return torch.zeros( |
LiftLayer and the CAN attention modules call indices() on user-supplied neighborhood matrices. Sparse matrices built with toponetx's from_sparse preserve explicit zero entries (e.g. zero-valued diagonal self-loops emitted by CellComplex.adjacency_matrix), so the number of stored entries does not match the number of true neighborhood relations:
Add a sanitize_neighborhood helper that coalesces and removes explicit zero entries, and apply it before all index access.
Closes #236 and closes #242