Skip to content

Read the JUMP-Lite embeddings, and document working with them - #102

Open
timtreis wants to merge 9 commits into
mainfrom
feat/jump-lite-embeddings
Open

timtreis wants to merge 9 commits into
mainfrom
feat/jump-lite-embeddings

Conversation

@timtreis

@timtreis timtreis commented Sep 20, 2026

Copy link
Copy Markdown
Member

Stacked on #100, with #101 merged in.

ds.jump_lite reads JUMP-Lite Target-2: four plates of the JUMP Target-2 plate map, one from each of four laboratories, with the same 1,536 wells measured six ways — five learned embeddings (OpenPhenom, DINOv2, an untrained DINOv2, SubCell, MorphEm) and cp_measure, the CellProfiler-equivalent. Rows and metadata are identical across all six, so model= changes the feature block and nothing else. CC0, 2.2 to 26 MB per set, pinned by sha256 like every other dataset. ds.jump_lite_targets returns the RefChemDB gene annotations in the shape metrics.known_relationships reads.

Tutorial 12 is the reason for it. The package had no page on learned embeddings, and nothing in it assumes CellProfiler features, so the page is about what actually differs: var carries no compartment or channel, which retires feature families, feature sets and the blocklist; normalization is unchanged; feature selection is skipped, because the dimensions are a basis rather than measurements that can be dropped one at a time. io.stamp brings in a matrix produced elsewhere.

The comparison scores three alignments — principal components, pp.tvn and pp.harmony — on recall of compounds sharing an annotated target and on replicate retrieval across the four laboratories. They disagree: Harmony leaves the least batch structure behind and gives the best replicate retrieval, while TVN is the only one that lifts target recall above the 10% baseline. The page says that rather than presenting either as the default, and draws both embeddings, because the contrast is the lesson.

It also keeps the untrained model in the comparison. Its replicate retrieval collapses where the trained models hold, under every alignment, which is the cheapest check that a pipeline measures anything at all.

Chasing the detached cluster in the TVN embedding found sixteen wells with a median of twelve cells against 252 elsewhere — a QC failure no correction fixes and neither readout reports. That is in the page too.

This is also what corrected #100: cp_measure gains as much from TVN as the learned sets do, so the earlier reading that TVN fails on CellProfiler features was wrong.

Verified: 815 tests pass, mypy, ruff and the formatters clean. The notebook executes end to end in the tutorials environment with no errors, and its committed outputs come from that run.

@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@timtreis
timtreis force-pushed the feat/jump-lite-embeddings branch 2 times, most recently from 844221d to d44c07b Compare September 20, 2026 21:23
@timtreis
timtreis force-pushed the feat/jump-lite-embeddings branch from 751a264 to 5997d44 Compare September 20, 2026 21:53
@timtreis
timtreis force-pushed the feat/jump-lite-embeddings branch from 5997d44 to 1e299ca Compare September 21, 2026 00:04
@timtreis
timtreis force-pushed the feat/jump-lite-embeddings branch from 1e299ca to 708ec0d Compare September 21, 2026 00:12
Base automatically changed from feat/tvn-coral to main September 21, 2026 01:07
@codecov-commenter

codecov-commenter commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.32258% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.79%. Comparing base (64b7f43) to head (d5680a8).

Files with missing lines Patch % Lines
src/mantispy/ds/_datasets.py 76.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #102      +/-   ##
==========================================
+ Coverage   87.75%   87.79%   +0.03%     
==========================================
  Files          81       81              
  Lines        6315     6360      +45     
==========================================
+ Hits         5542     5584      +42     
- Misses        773      776       +3     
Files with missing lines Coverage Δ
src/mantispy/_core/features.py 96.72% <100.00%> (+0.56%) ⬆️
src/mantispy/ds/__init__.py 100.00% <ø> (ø)
src/mantispy/pp/_batch.py 84.45% <ø> (ø)
src/mantispy/pp/_select.py 100.00% <100.00%> (ø)
src/mantispy/pp/_well_qc.py 100.00% <100.00%> (ø)
src/mantispy/tl/_design.py 94.05% <ø> (ø)
src/mantispy/ds/_datasets.py 29.61% <76.00%> (+6.21%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Every other page measures cells with CellProfiler. This one swaps the feature
block for a neural network's output on the same wells and says what changes:
var carries no compartment or channel, so feature families, feature sets and
the blocklist do not apply; normalization is unchanged; and feature selection
is skipped, because the dimensions of an embedding are a basis rather than a
list of measurements that can be dropped one at a time.

It compares all six JUMP-Lite feature sets on target recall and replicate
retrieval, keeps the untrained model in the comparison as the control that
says whether the pipeline measures anything, and shows the trade between
batch alignment and replicate agreement rather than presenting one of them as
the default.
The page compared principal components against TVN and left the reader with
a UMAP whose sources still separate, having shown no method that fixes it.
Harmony does: over the six feature sets it leaves the least batch structure
behind and gives the best replicate retrieval, while TVN remains the only one
that lifts recall of annotated targets above chance. Neither buys the other's
gain, so the page now says that rather than presenting one as the default.

Both embeddings are drawn, because the contrast is the lesson. Chasing the
detached cluster in the TVN panel found sixteen wells with a median of twelve
cells against 252 elsewhere, which no correction fixes and neither readout in
the table reports.
The page treated the laboratory as the nuisance to correct. Measured with the
same pc_regression, the cell count explains seven to twelve times more of the
variance than the source does on every trained embedding, and lands on the
first component. The untrained model does not do it and cp_measure does not
do it, so this is something the models learned and something averaging
per-cell measurements removes — which makes it the sharpest difference
between the two kinds of feature, and the reason the alignments moved target
recall so little.

Whether to act on it is measured rather than assumed. The thin wells are
cytotoxic compounds, thin at more than one laboratory independently, so a
cell-count floor would delete the largest effects; but two thin wells of
different compounds are nearly as alike as replicates of the same one, so
what they reproduce is mostly emptiness. Regressing the count out raises
target recall under every alignment and takes replicate retrieval apart on
two of the three. The page keeps every well and says why: for a compound the
cell count is partly a treatment effect, which is why the JUMP recipe
regresses it for the genetic arms and not for compounds.

Also answers what transfers from scanpy — the count-based filters do not, the
reductions do — and drops a private tl._similarity import for mt.tl.similarity.
The cell count dominating a learned embedding was reachable only by reading
tutorial 12 to the end, and it changes how three existing functions should be
used.

pp.regress_out defaults to keys=("Metadata_CellCount",), which is the call
the tutorial argues against for a compound screen, and said nothing about it;
it now records that the JUMP recipe regresses the count out of the genetic
arms and not the compound one, and why.

ds.jump_lite records that every trained set here puts the count on its first
component while the untrained one and cp_measure do not, since that is a
property of the data a reader will meet. tl.cytotoxicity records that the
confound is larger on an embedding than on a CellProfiler profile.
…cells

ds.jump_lite went straight through read_profiles, so the CellProfiler name
parser ran on names with no structure: openphenom_nahualX_17 became the
nahualX feature group of an openphenom object, and scale became the dimension
index. The docstring and the tutorial both said the opposite of what the code
did. cp_measure is real measurements and keeps its annotation.

The tutorial asserted several things no cell computed, and contradicted its own
tables in four places. Each claim now sits in the section that measures it: the
source variance is reported for every model and alignment where the question is
asked, the cell count's place on the first component is shown per component
rather than asserted, and the island in the TVN panel is named by clustering
instead of from memory.

jump_lite_targets drops missing genes before the cast, or every unannotated
compound becomes the string 'nan' and they all count as a related pair. The CI
cache script downloads the feature sets the tutorial reads, and the registry
shape test knows JUMP-Lite publishes one count per well rather than per field.
…he contrast

The CellProfiler grammar read cp_measure's `nuclei_3/max/radial_zernikesRadialDistribution_ZernikeMagnitude_9_9`
as a group of `0/max/intensityIntensity` with no channel at all, so the one block on the page that was
supposed to carry a compartment, a group and a channel carried none of them. Parse that grammar instead; the
channel stays the index cp_measure numbers its inputs by, because the stain it stands for is not in the name.

Tutorial 12 then rests on measurements rather than assertions. The CellProfiler block gets the standard
pycytominer selection plus drop_outliers, without which a handful of unbounded shape moments are most of its
variance and every number computed from it is about those moments. The chance level for
`metrics.known_relationships` is measured per representation by shuffling the annotation rather than assumed
to be 10%, which is what the untrained control's apparently high recall turned out to be. Replicate retrieval
is `tl.map` with a cross-laboratory pair rule, which is what the prose already claimed and what scales past a
dense similarity matrix. New sections describe the plates before analysing them and put the CellProfiler
names back on the embedding's axes by regressing one block on the other.

`well_qc` counted one row per well and failed every well on a well-level object, which the page recommended;
it now declares the resolution it expects. `feature_select` warns when it selects nothing instead of handing
an empty matrix to whatever runs next, which is how noise_removal fails here: its cutoff is an absolute
threshold on the scale `normalize` left the values on.
@timtreis
timtreis force-pushed the feat/jump-lite-embeddings branch from f94645a to 1c63b33 Compare September 21, 2026 03:17
…eady scored

parse_feature_names is handed a whole column list, and the grammar is a property
of the file that wrote it rather than of any one column, so decide it once over
the list instead of probing every name inside _parse_one. A name that does not
fit the chosen grammar is then a mixed file rather than something to guess at,
and _read_suffixes takes the group rather than a flag derived from it at each
call site.

In the tutorial, the per-compound panels reuse the mAP the comparison table
already computed rather than running the heaviest metric again for six of its
eighteen conditions, the cell-count section reads obs off the block already in
hand, the bridge branches off the normalized CellProfiler block instead of
re-reading it, and the per-component variance table calls
metrics.batch_variance_explained rather than stacking pc_regression by hand.
Every number is unchanged.
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