Skip to content

Spatial Genomics ingest

Load one ROI of a Spatial Genomics (GenePS / sganalysis) export into NimbusImage. Needs pip install 'nimbusimage[sg]'. Every step is also a subcommand of the nimbusimage-sg command.

nimbusimage.sg.export

Reading one ROI of a Spatial Genomics (GenePS / sganalysis) data export.

An export holds one project folder with two trees per ROI::

<project>/basic_results/roi_N/
    roi_N_Boundaries.geojson     cell polygons, MICRONS, id = cell label
    roi_N_CellCoordinates.csv    label, area (um^2), center_x, center_y
    roi_N_CellxGene.csv          raw counts, one row per label
    roi_N_TranscriptList.csv     name, x, y, z (microns)
    roi_N_Segmentation.tiff      uint32 cell-label mask
    roi_N_DAPI.tiff              duplicate of images/dapi.tiff
<project>/mini_project/roi_N/
    images/*.tiff                OME-TIFF channels (dapi, GFP_image, ...)
    clusters/matrix_*_cells.h5ad AnnData: obs leiden, obsm X_umap
    genes/<project>.hdf5         per-gene points in PIXELS (+ UNUSED*)
    sganalysis.session           JSON: shape, res (um / pixel)

The cell label is the join key everywhere: the GeoJSON feature id, the CellxGene row index, the h5ad obs index and the segmentation mask value.

SGRoi

One ROI of a Spatial Genomics export.

SGRoi(export_dir, "roi_3"); export_dir may be the project folder or a parent of it (see :func:find_project_dir). Files are read lazily, and a missing one raises :class:SGError.

clusters_path property

The cells' h5ad (clusters/matrix_*_cells.h5ad), or None.

pixel_size property

Microns per image pixel (res in sganalysis.session).

shape property

Image (rows, cols) in pixels.

image_paths()

(channel name, path) for each image, DAPI first.

polygons()

(labels, rings): each ring is an [n, 2] array of micron vertices with the closing vertex dropped; outer ring only.

coordinates()

CellCoordinates as a DataFrame indexed by label: area (um^2), center_x, center_y (microns).

counts()

(labels, symbols, names, counts): raw counts as a cells x genes scipy.sparse.csr_matrix; symbols are the names as NimbusImage stores them (:func:safe_symbol).

clusters()

From the cells' h5ad: labels, leiden (int, -1 for missing), umap ([n, 2] or None) and gene_ids ({name: Ensembl id}). None when the export has no h5ad.

transcripts()

(gene_names, gene_codes, xyz): sorted names (stored form, :func:safe_symbol, matching the table's symbols), an int index into them per molecule, and [n, 3] float32 microns.

find_project_dir(path)

The folder holding basic_results/ and mini_project/.

Accepts that folder itself or any parent up to two levels above it (DataExport_X/ or the folder the zip was extracted into), as long as exactly one project folder is found.

list_rois(path)

The ROI names of an export (roi_1, roi_2, ...) in order.

channel_name(path)

dapi.tiff -> DAPI; GFP_image.tiff -> GFP.

NimbusImage's filename parser splits on _, so the name must not contain one.

nimbusimage.sg.cells

The cell map: Spatial Genomics cell label -> NimbusImage annotation id.

Saved by upload_polygons together with what it belongs to — the dataset, the ROI and project, and the pixel size the polygons were drawn with — so a later step can refuse a map of another dataset or ROI before writing anything.

SGCellMap

labels[i] was uploaded as annotation ids[i].

check(ds, roi=None)

Refuse a map of another dataset, ROI, or pixel size.

verify_cells(ds, roi, cells, sample=50, seed=0)

Read back a random sample of the map's annotations and compare each first vertex with the GeoJSON's (at cells.pixel_size). Returns the number checked; raises on any mismatch or missing annotation.

nimbusimage.sg.ingest

Upload steps of the Spatial Genomics ingest.

Reuses the Xenium machinery where the data model is the same (channel staging, polygon annotations, property helpers, the spatial-table upload that refuses another dataset's table); what differs is how each input is read and that the transcript list must be converted into a store.

upload_images(client, roi, name, parent_folder_id=None, wait=True)

A dataset from the ROI's mini_project/roi_N/images, one channel per file (DAPI first), named by file (GFP_image -> GFP). Waits for the transcode unless wait=False.

upload_polygons(ds, roi, *, tags=None, batch=5000, limit=None)

Bulk-create the ROI's cell polygons (px = um / pixel_size).

Returns the cell map; save it (.save(path)), every later step takes it. Polygons with fewer than three vertices are skipped.

upload_cell_data(ds, roi, cells, *, base_tags=None, chunk=5000, verify_sample=20)

From the export: per cell

  • tags base_tags + ["Leiden k"] (categorical, for the tag filter and selection summary; replaces the cell's tags). base_tags defaults to the tags the polygons were created with (cells.tags),
  • SG Area / area_um2 (CellCoordinates),
  • UMAP / x, y and Clustering / leiden (the h5ad) — the numeric cluster is what Color by Property (and so the overview raster) can color by.

Property values are replaced, not merged (a re-run is exact). Returns cluster sizes.

build_spatial_table(roi, cells, out)

Write the dataset's spatial.zarr.zip from CellxGene.csv.

::

X              raw counts, cells x genes, CSC
layers/X_csr   the same, CSR
obs            annotation_id (the join key), cell_index (= label),
               cell_type ("Leiden k"), leiden
var            index = symbol; gene_id (Ensembl, from the h5ad);
               feature_type "gene"; feature_name (as exported)
obsm/X_umap    when the h5ad has one
uns/nimbus     schemaVersion, datasetId, source, created

Rows are the mapped cells (others are dropped and logged).

upload_spatial_table(ds, path)

Upload and register a table built for ds (refuses another dataset's).

build_transcript_store(roi, out)

Convert TranscriptList.csv into a registrable transcript store (see :mod:nimbusimage.sg.transcripts).

transcript_store_source(path)

The nimbus_source a store was written with ({} if none).

register_transcripts(ds, cells, path)

Upload a store from :func:build_transcript_store and register it at the cell map's pixel size — the frame the polygons were drawn in. Refuses a store written for another ROI.

nimbusimage.sg.transcripts

Write molecules as a transcript store the SpatialPlugin can register.

The plugin reads one layout, 10x Xenium's transcripts.zarr.zip (see codebaseDocumentation/SPATIAL_PLUGIN.md, "the transcript layer"), so a Spatial Genomics transcript list is rewritten into it::

attrs gene_names
grids attrs number_levels, grid_size [250], grid_keys, grid_number_objects
grids/{L}/{gx},{gy}   tiles of 250 um * 2**L from the origin, rows sorted
                      by gene; gene_offset[g] = [lowStart, lowEnd,
                      highStart, highEnd]
  level 0             location (x, y, z um), quality_score,
                      gene_identity, id
  level L > 0         each gene's molecules merged on a grid of
                      0.5 um * 2**L: location (centroid),
                      gene_identity, cluster_count
density/gene          attrs rows, cols, grid_size [10]; CSR over
                      (gene * rows + row) of a 10 um grid

The store is written in zarr format 2 (the server reads with zarr 2), through either zarr 2 or zarr 3 on the client.

write_transcript_store(gene_names, genes, xyz, out, *, source=None, quality=DEFAULT_QUALITY, levels=LEVELS)

Write out (a .zarr.zip) from per-molecule gene indices into gene_names and [n, 3] micron positions. source is stored as the root attribute nimbus_source (register_transcripts checks it against the cell map). Returns out.

nimbusimage.sg.verify

Local checks of an export before (and independent of) any upload.

  • :func:check_frame: cell centroids land on their own label in the segmentation mask at floor(um / pixel_size) — the frame the polygons and molecules will be drawn in.
  • :func:recount: assigning every molecule to the mask label under it reproduces CellxGene.csv — molecules, cells and the matrix agree.

Mask lookups decode only the tiles they touch, so neither check reads a whole (up to ~11 GB) label image.

labels_at(path, x, y)

Values of a (tiled) label TIFF's full-resolution level at integer pixel (x, y); out-of-image positions give 0.

check_frame(roi, sample=300, seed=0)

Fraction of sampled cell centroids that land on their own label.

recount(roi)

Reassign every molecule by the mask and compare with CellxGene.csv.

Returns molecule totals (inside cells, and in the matrix), the fraction of identical matrix entries, the correlation, and the largest per-entry difference.

nimbusimage.sg.errors

Errors raised by the Spatial Genomics ingest.

SGError

Bases: Exception

A Spatial Genomics export, input file, or upload does not match expectations.

Raised instead of writing wrong data: a missing file, labels that do not join, a cell map of another dataset or ROI, a server that created fewer annotations than were sent.