v0.7.1 MERFISH and CosMX capabilities - #50
Merged
Conversation
msraredon
commented
Aug 6, 2026
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 |
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>
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.