methods for calculating radial expansion - #315
Conversation
Summary: Nuclei Radial Expansion & Gene Expression (DEGA-545)PurposeAdd a generic way to grow a "neighborhood" entity (e.g. a segmented nucleus) outward in fixed radial steps until it reaches a bounding entity (e.g. its cell), and compute a gene-expression matrix at each step using the New public API
|
| File | Change |
|---|---|
src/celldega/nbhd/expansion.py |
New. _calc_expansion engine (155 lines). |
src/celldega/nbhd/trx_streaming.py |
New. _assign_trx_to_entity_streaming_parquet engine (139 lines). |
src/celldega/nbhd/collection.py |
Adds calc_expansion; calc_signature gains feature_col/x_col/y_col; calc_transcript_assignment unchanged. |
src/celldega/nbhd/neighborhoods.py |
_calc_nbhd_by_gene's cell-free path simplified to a single streaming data_dir branch (net: 30 insertions, 44 deletions — smaller than main). |
src/celldega/nbhd/utils.py |
Adds safe_polygon, simple_format, transform_polygon, make_column_names_unique_fast. |
src/celldega/nbhd/__init__.py |
Exports the four new utils functions. |
tests/unit/test_nbhd/test_expansion.py |
New. Covers calc_expansion, scale handling, and calc_signature cell-free paths. |
tests/unit/test_nbhd/test_trx_streaming.py |
New. Covers the streaming engine directly, including custom column names. |
tests/unit/test_nbhd/test_utils.py |
New. Covers the four utils helpers. |
docs/examples/brief_notebooks/Nuclear_Expansion_Radial_Buffering.ipynb |
New. Runnable, executed example (synthetic data): build nuclei/cells → calc_expansion → synthetic transcripts → calc_signature(by="cell-free", data_dir=...) per radius. |
Net diff vs. main: 10 files, +1410/-49 lines.
Verification
- 47 unit tests pass (
pytest tests/unit/test_nbhd/). ruff checkclean.- Example notebook executes end-to-end with no errors after every revision.
There was a problem hiding this comment.
Comments
1. Do we need gdf_bounds clipping at all?
Right now every buffered entity is clipped to a matching row in a separate
"parent" boundary GeoDataFrame (e.g. nucleus → cell). I don't think that
we need this:
- One of the premises of buffering outward from a nucleus is that the existing
cell segmentation is imperfect — so we should not cap growth exactly at
that boundary - It assumes every entity has a natural 1:1 parent to clip to. That's true for
nucleus→cell, but not for the more general case of buffering cells
themselves (no larger "container" to clip to). - It seems to sidestep the more important problem — preventing entities from
clashing into each other as they grow. I think we should design mutual
clash/overlap prevention between entities and drop the parent-boundary
requirement (or make it optional).
2. Naming
Given the potential generic use case — generate shrunk/grown versions of cells (buffer
in and out) and investigate how the per-cell gene signature changes as a
means to get a handle on spillover — calc_expansion doesn't quite capture the
"grow or shrink" symmetry.
Something like cell_buffer (signed distance, +out/-in) maybe.
3. Does this belong in nbhd?
I am thinking again that we probably should consider making a dega.cell module,
separate from dega.nbhd? Cells and neighborhoods have different assumptions
(e.g., cell is a contiguous polygon, neighborhood can be many discontiguous polygons
or points). If we had adega.cell module, buffering would be a natural first method
there, with potential room to grow into other cell-level analyses later:
- morphology (shape descriptors, elongation, etc.)
- local density / crowding
- cell-cell communication between specific pairs
- sub-cellular analysis (à la Bento)
If we go this route, some of what's already in nbhd (transcript streaming/
assignment, technology-aware scale + column handling) is generic enough to
lift into a shared utils layer both cell and nbhd pull from, rather than
duplicating it.
4. Raw transcript wiring
Related to the spillover use case: this needs raw transcript positions
per-technology (not just Xenium's column names), so whatever we land on for
loading transcripts should be technology-aware from the start rather than
requiring callers to pass platform-specific column names by hand. Maybe we can
interact with SpatialData as an intermediate?
Summary of the pull request is provided in a separate comment below.