smooth-all-once: derive any signature from one full-panel smooth (+ groupby cache fix, tutorial sparse-gene rescue) - #3
Merged
Conversation
Add all_genes=True (and smooth_all()) so the pipeline smooths every var once and any signature/single-gene/blend is gathered from those full layers -- the cache key stops depending on the requested subset, so the diffusion-map GP runs exactly once across spatial/dm/composed/blend. Exact for the linear neighbour smoothers; float-precision for the GP (mellon's Nystrom solve rounds with matrix width). Fold obs[groupby] contents into _cache_key: a condition-aware KompotGP fitted on obs[groupby]==condition was served a stale hit when that column was mutated in place (same name, different assignment). Now a changed grouping misses. Tutorial: add a sparse hippocampal member (ranked by detection rate) and show raw speckle -> smoothed rescue; use blend for the combined view; smooth all genes once up front and derive every mode cheaply, with a check that the GP ran once. Notebook regenerated unexecuted (executed re-run needs the data host).
katosh
added a commit
that referenced
this pull request
Jul 21, 2026
Since v0.1.0 the package gained three user-facing features -- `blend` composition mode (#1), the smoothing reuse cache (#2), and smooth-all-once `all_genes=True` (#3) -- so a MINOR bump to 0.2.0 is the correct SemVer step. - pyproject.toml + src/spatial_smooth/__init__.py: version 0.1.0 -> 0.2.0 - docs/source/installation.rst: the illustrative check_dependencies() output shows v0.2.0 (it prints v{__version__}) - CHANGELOG.md: new; 0.2.0 (the three features + the tutorial-now-executed and PyPI-image fixes) and a 0.1.0 baseline entry conf.py already tracks __version__, so the docs title/version follow automatically. twine check passes on the 0.2.0 sdist+wheel; the full test suite is green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Computing signature A (genes 1–5) then signature B (genes 3–8) through the same pipeline used to
run two smooths — the #2 cache keys on the requested subset, so partial overlap was never
reused. This PR adds a smooth-all-genes-once path so the expensive smoothing is done a single
time per view and every signature, single gene, and
blendis derived from those pre-smoothedlayers with no recompute. It also folds in a cache-correctness fix surfaced by the #2 skeptic, and
reworks the tutorial around the new capability (plus a sparse-gene rescue).
1. The rethink —
all_genes=Trueandsmooth_all()Mechanism (deliberately minimal — it extends the #2 cache, doesn't fight it).
smooth(..., all_genes=True)runs the pipeline over the full(n_obs, n_vars)matrix instead of the requestedsubset, then gathers the signature's columns out of the full smoothed result for scoring. Because
each step now consumes the whole matrix, the per-step cache key (
_cache_key) stops dependingon which genes were asked for — so a second signature, a single gene, or
blendthrough the samepipeline collides on the same key and re-smooths nothing.
smooth_all(adata, steps=...)is aconvenience pre-pass that warms those layers without scoring a throwaway signature.
How it composes with
blendand the cache.blend's two branches (spatial,dm) run on theraw matrix independently; with
all_genes=Trueeach branch is exactly the full-panelspatial/dmsmooth, so a priorsmooth_allfor those two views serves both branches — thediffusion-map GP runs exactly once across
spatial,dm,dm+spatial,blend, and any singlegene. Verified end-to-end by call-counting (
test_all_genes_gp_runs_once_across_dm_gene_and_blend).Fidelity — scoped precisely (this is the one place I did not claim "byte-identical" blanket).
The neighbour smoothers (
KnnGaussian,Kde) are linear and column-independent, so a signaturegathered from the all-genes layer is bit-for-bit the per-subset smooth
(
test_all_genes_linear_is_exact,assert_array_equal). The GP (KompotGP) matches the per-subsetresult to floating-point precision (~1e-13 in float64; exact once stored as float32) — not
bit-identical, because mellon's Nyström solve batches the columns and a 248-wide solve rounds a
shade differently from a 3-wide one. Every signature derived from one full layer is mutually
exact; the only non-exact comparison is against the counterfactual per-subset path we're replacing.
Tests assert
allclose(atol=1e-6)for the GP and are explicit about why.Scoring is unchanged:
mean_zstill standardises against the signature's raw subset stats;all_geneschanges only the source of the smoothed values. Storage footprint is unchanged too —the #2 cache already allocates full-width
(n_obs, n_vars)layers;all_genesfills every columninstead of some, and stores fewer distinct entries (one per pipeline, not one per signature).
2. Cache-correctness fix (#2 skeptic):
groupbycontents in the keyA condition-aware
KompotGP(groupby=..., condition=...)fits only onobs[groupby] == condition._cache_keyhashed thegroupbycolumn name and theconditionlabel but not the column'scontents — so mutating that grouping in place (same name, different assignment) between two
otherwise-identical calls served a stale result fitted on the old grouping. The key now folds
the column's values (NUL-joined bytes) in, gated on
groupbybeing set (keys of non-grouped stepsare unaffected). Tests:
test_cache_key_folds_groupby_column_contents,test_cache_key_ignores_unrelated_obs_for_non_groupby_steps, and an end-to-endtest_mutated_grouping_misses_and_recomputesreproducing the old false-hit → now a miss.3. Tutorial
fraction of cells with any counts); the sparsest present marker is spotlighted and shown
raw (speckle) → smoothed (the hippocampal domain re-emerges). The candidate list includes
Ascl1(the colab's sparse marker, ~19% there) but is robust: it keeps whichever markers the panel
carries and picks the sparsest, computing and printing the real detection rate at runtime.
blendis the combined method (replaces the old compose/dm+spatial framing for "both").all_genes=True, with acheap check that counts distinct cached smooths —
kompot_gpappears exactly once — and abefore/after check that
blendreuses it.Notebook execution — follow-up.
notebooks/tutorial.ipynbis regenerated from source butunexecuted (I had no data host to run the public Xenium fetch + Palantir diffusion map). The
previously-committed executed outputs referenced the old signature/sections and would be stale, so
shipping a fresh unexecuted notebook is the honest state. The executed artifact (figures + the
real sparse-gene detection rate) needs a one-time re-run on a machine with data access — same path
the blend PR took. The
test_tutorial_artifactexecuted-only checksskip(not fail) until then.Tests
Full suite green: 161 passed, 6 skipped (3 squidpy env-only + 3 executed-notebook checks that
skip on the unexecuted artifact) via
PYTHONPATH=src <venv>/bin/python -m pytest. New:tests/test_all_genes.py(linear exactness, cross-signature reuse, single-gene rescue,smooth_allwarm/no-score, GP-runs-once, GP precision, blend parity) and the fourgroupby-keytests in
tests/test_cache.py.Do not merge — for architecture review.