Skip to content

v0.7.1 MERFISH and CosMX capabilities - #50

Merged
msraredon merged 3 commits into
RaredonLab:devfrom
msraredon:dev
Aug 6, 2026
Merged

v0.7.1 MERFISH and CosMX capabilities#50
msraredon merged 3 commits into
RaredonLab:devfrom
msraredon:dev

Conversation

@msraredon

Copy link
Copy Markdown
Collaborator
Platform Dataset Boundaries
Xenium mouse ileum (committed) + human breast
seqFISH synthetic (committed) + Spatial Genomics reference
Visium HD 10x Tiny Mouse Brain (committed, CC BY 4.0) ✓ bins as squares
MERSCOPE Vizgen VPT test data ✓ geoparquet WKB
CosMx OSTA/OSF mouse brain ✓ polygons CSV

msraredon and others added 3 commits August 6, 2026 12:35
Both readers were written from documentation and had never run against a real
dataset. Doing so found five defects, four of which rendered the platform
unusable rather than merely degraded. Verified against Vizgen's own VPT test
data (Apache-2.0) and the OSTA/OSF CosMx mouse brain set; neither is committed,
and sample_data/.gitignore already excluded both patterns.

MERSCOPE

transcripts() read usecols=["x","y"] from detected_transcripts.csv. Those are
FOV-LOCAL PIXEL coordinates; the whole-slide micron pair is global_x/global_y.
Measured on the real file, transcripts came back spanning 843-18111 px while
the cells they belong to spanned 16-3872 -- a 4.7x mismatch that put the
transcript layer nowhere near the tissue. It was invisible because the method
was wrapped in a bare `except Exception` returning an empty result. Now reads
global_x/global_y, and says so when they are absent instead of silently
returning nothing.

pixel_size looked for microns_per_pixel in a root manifest.json. The real
source is images/micron_to_mosaic_pixel_transform.csv, whose scale term is
pixels per micron, so pixel_size = 1/M[0][0]. Reading it gives 0.107962 on the
reference data; the old 0.108 fallback was accidentally correct, which is why
nothing looked wrong.

Boundaries now work. The "HDF5 only" blocker applied to instrument software
v231 and earlier; v232+ writes geoparquet, and vizgen-postprocessing writes
cell_micron_space.parquet. Both are WKB polygons already in microns. Decoded
with ~40 lines of struct unpacking against the OGC spec rather than adding
shapely, a C-extension dependency, to a requirements file pinned tightly
enough to need cffi<2.0 for pyvips. Validated geometrically: 907 of 918
reported cell centres fall inside their own decoded polygon, median offset
1.4 um. The 11 misses are cells whose volumetric centroid across 7 z-planes
sits outside the single z-plane drawn -- expected, not a decode error.

CosMx

pixel_size was 0.18; Bruker documents 0.12028 (120 nm edge) and Giotto
hardcodes the same. 50% too large, which scaled every distance and, since the
edge reader divides x1/y1 by pixel_size, would have displaced the whole edge
layer.

Detection never fired on real data. Some public exports drop the experiment
prefix, and `*_tx_file.csv` cannot match a bare `tx_file.csv` -- the glob needs
a literal underscore. Several sentinels are now tried, since exports vary in
which files ship. _find_file retries without the prefix for the same reason.

The same prefix assumption made the reader hang. _ROOT_CSV_SKIP_SUFFIXES only
listed prefixed names, so the shared supplemental-metadata loader treated the
platform's own exprMat (73 MB), polygons (38 MB) and tx (24 MB) as user
metadata and tried to outer-join them. cells() took over 90 s; it now takes
0.2 s.

Cell metadata never loaded even so: the rename looked for "x_centroid" but
CosMx writes CenterX_global_px. Cells are also keyed on the composite
(fov, cell_ID) -- cell_ID restarts at 1 in every field of view, so keying on it
alone merges unrelated cells. Newer exports' study-wide `cell` column is
preferred when present, with a local+FOV-origin fallback for exports carrying
only the per-FOV frame.

Boundaries now work from <expt>-polygons.csv, which is a standard AtoMx export
and gives vertices already in global pixels. Note the column is `cellID` there
and `cell_ID` everywhere else. Validated: 4000/4000 sampled cell centres fall
inside their own polygon.

has_morphology is now conditional on CellComposite/ rather than always True --
the OSF export has no images.

Guard grows to 171 probes across 7 datasets. All five pre-existing datasets are
byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Browser verification of CosMx and MERSCOPE. MERSCOPE was correct as committed;
CosMx showed a blank canvas, and the cause was architectural rather than
CosMx-specific.

The viewer derives its entire coordinate space from the tile pyramid: OSD's
`open` event is the only thing that ever calls setImageSize, and deck.gl mounts
only once imageSize is known. A dataset with no morphology therefore rendered
nothing at all -- no cells, no boundaries, no edges -- even though every
endpoint served correct data. Any image-less dataset hit this, not just CosMx;
it was latent because every dataset until now had an image.

Rather than give deck.gl its own navigation path when OSD is absent, which
would fork the model that per-panel rotation and match-zoom both depend on, the
backend now synthesises a placeholder canvas. list_images offers a reserved
"__blank__" entry when a dataset has no image of its own, and the tiles router
sizes it from the reader's new data_extent(). No tiles are generated: a blank
pyramid is uniform, so one 256x256 tile is written and served for every
level/column/row. At CosMx scale that is 8 KB instead of tens of thousands of
identical files.

CosMx coordinates are now normalised to the dataset's own origin. Its global
pixel coordinates are in the slide frame, so the mouse-brain set spans
x 128749..175643 and y -9980..15588. Anchoring the canvas at the slide origin
put 43% of cells at negative coordinates and off the canvas entirely, with the
data occupying 27% of the width. TissuePlex's contract is image pixel space and
CosMx ships no image to define it, so with no external frame the dataset's own
corner is the only sensible origin. Cells, transcripts and boundaries subtract
the same offset and stay registered.

Two bugs fell out of testing that.

cell_boundaries filtered the bbox against raw slide-frame coordinates while the
incoming bbox was already in the shifted frame, so full-extent queries worked
and every viewport query returned zero. It also applied _bbox_to_native, which
multiplies by pixel_size for micron platforms -- wrong for CosMx, whose
coordinates are already pixels.

activeImage was only reconciled inside the image-list fetch, whose closure
captured it and which only re-runs on dataset change. Whether it got set
depended on the order two state updates landed in, and for a dataset whose only
image is the placeholder it stayed null, so OSD never opened. Reconciliation is
now its own declarative effect keyed on the image list.

Also: the Morphology layer row is gated on has_morphology, since the opacity
control over a flat placeholder fill is a dead toggle; and the placeholder name
is hidden from the viewer header rather than shown as if it were a real image.

Verified in the browser. MERSCOPE: 731/731 segments with real WKB-decoded
outlines, transcripts sitting inside their cells (the coordinate fix), DAPI
pyramid rendering, cell click returning real expression. CosMx: 5k/39k segments
drawn on the placeholder canvas, 960 genes, no console or backend errors.
Guard holds at 171 probes; only the eight CosMx probes affected by the origin
shift moved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CosMx and MERSCOPE fixed against real public data, plus placeholder-canvas
rendering for datasets that ship no morphology image.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@msraredon
msraredon merged commit b693261 into RaredonLab:dev Aug 6, 2026
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.

1 participant