Skip to content

methods for calculating radial expansion - #315

Open
jaspreetishar wants to merge 15 commits into
mainfrom
DEGA-545-Nuclei-radial-buffering
Open

methods for calculating radial expansion#315
jaspreetishar wants to merge 15 commits into
mainfrom
DEGA-545-Nuclei-radial-buffering

Conversation

@jaspreetishar

@jaspreetishar jaspreetishar commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary of the pull request is provided in a separate comment below.

@jaspreetishar jaspreetishar changed the title methods for calculating radial expansion; example notebook; unit tests methods for calculating radial expansion Jul 14, 2026
@jaspreetishar

jaspreetishar commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Summary: Nuclei Radial Expansion & Gene Expression (DEGA-545)

Purpose

Add 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 celldega.nbhd API. Kept deliberately minimal: only the expansion engine is substantial new code; everything else slots into methods that already existed.

New public API

NeighborhoodCollection.calc_expansion(gdf_bounds, radii_um=(0.5, 1, 1.5, 2, 2.5), ...)

New file: src/celldega/nbhd/expansion.py (engine: _calc_expansion), wired into src/celldega/nbhd/collection.py.

  • Buffers every entity in a collection outward at each radius, clipping each to a matching row in gdf_bounds. Returns one new NeighborhoodCollection per radius, sharing an observation axis so downstream results stay comparable across radii.
  • Deliberately generic — not nucleus/cell-specific.
  • Scale handling: a single scale_um_per_pixel parameter (default 1.0, meaning geometry is already in microns). Pass technology="Xenium" as a shortcut if scaling factor for a Xenium dataset is required, or scale_um_per_pixel=1/pixels_per_micron if any other technology is being used.
  • _DEFAULT_RADII_UM = (0.5, 1, 1.5, 2, 2.5).

NeighborhoodCollection.calc_signature(by="cell-free", data_dir=..., feature_col=..., x_col=..., y_col=...)

Modified: src/celldega/nbhd/neighborhoods.py (_calc_nbhd_by_gene), src/celldega/nbhd/collection.py.

  • Cell-free transcript counting: data_dir (a directory containing a transcripts.parquet), always streamed in batches through the engine below. feature_col/x_col/y_col name its gene/x/y columns (Xenium convention by default — feature_name/x_location/y_location — but overridable for any column layout).
  • by="cell" (cell-derived mean expression) is unchanged from main.

Streaming transcript-to-entity assignment

New file: src/celldega/nbhd/trx_streaming.py.

  • Reads a transcripts parquet file in batches via pyarrow; each batch's candidate entities are narrowed with the entity GeoDataFrame's spatial index before an exact point-in-polygon test. Memory use stays bounded by batch size regardless of file size.
  • Backs calc_signature's data_dir= path unconditionally.

New celldega.nbhd.utils helpers (exported from celldega.nbhd)

  • safe_polygon, simple_format, transform_polygon, make_column_names_unique_fast — carried over as-is as helpers from the user's own preprocessing notebook.

Files changed (vs. main)

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 check clean.
  • Example notebook executes end-to-end with no errors after every revision.

@jaspreetishar
jaspreetishar marked this pull request as ready for review July 17, 2026 19:45

@cornhundred cornhundred left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants