Skip to content

Python package reference

Generated from the docstrings in the package source. The modules below are the ones that exist; the API client lands with the API.

Installing it, the interpreter it needs and the licence it is under are on the Quickstart, which is also the shortest path through what follows. The whole of it needs only NumPy, h5py, OpenCV and Pillow.

Quality control

Three predicates and the cascade that combines them. Every tile a model sees has passed all three, and a tile that did not can say which one it missed.

qc

Patch quality control: which tiles are worth predicting on.

A whole-slide image is mostly not tissue. Feeding the empty glass, the pen marks and the out-of-focus edges to a model wastes the run and drags every downstream summary toward the mean of nothing, so every candidate tile passes three independent predicates first:

foreground_mask How much of the tile is not slide background — the share of pixels darker than the white level. Rejects empty glass. blur_filter The variance of the tile's Laplacian, the standard sharpness proxy: a flat response means nothing in the tile has an edge. Rejects out-of-focus tiles. hsv_filter How much of the tile looks stained — pixels with enough saturation to carry dye and not so bright that they are glare. Rejects grey artefacts, which are dark (so they have foreground) and sharp (so they are not blurry) but carry no stain.

All three are ratios or variances of the tile itself, so a tile can be judged wherever it is decoded, with no slide reader and no model.

Higher is stricter for every threshold: each one is a floor the measured value must reach, so raising it keeps fewer tiles.

The stained-tissue predicate is the one that has to agree across implementations — the same rule decides which tiles a model sees and which tiles a reviewer is shown, in more than one language — so its two constants are named here and pinned by a shared vector fixture rather than restated in prose.

QcThresholds dataclass

The floors a tile must reach to be used.

The field names are the ones a model card's input_spec.qc block uses, so a card's own thresholds go straight in:

>>> QcThresholds.from_input_spec(card["input_spec"])   # doctest: +SKIP

The defaults are the values a card carries when it has no reason to differ, and they are what every predicate below falls back to.

field_names classmethod
field_names() -> tuple[str, ...]

The threshold keys, in declaration order.

from_input_spec classmethod
from_input_spec(input_spec: dict[str, Any]) -> QcThresholds

Build from a model card's input_spec.

Refuses a spec whose qc block names a threshold this version does not know, rather than silently ignoring it: a threshold the caller believes is being applied and is not is the one failure here that produces plausible-looking results from the wrong tiles.

as_dict
as_dict() -> dict[str, float]

The thresholds as a plain mapping, for reports and provenance.

QcResult dataclass

One tile's verdict and the three numbers behind it.

A statistic the cascade never reached is nan, not zero: zero is a measurement ("this tile has no stained pixels") and would be averaged into per-slide summaries as one.

rejected_by names the threshold that rejected the tile, using the :class:QcThresholds field name, and is None when the tile passed. passes is derived from it rather than stored, because the two must agree and a stored pair can be constructed disagreeing.

passes property
passes: bool

No threshold rejected this tile.

stat_names classmethod
stat_names() -> tuple[str, ...]

The three measured statistics, in declaration order.

Callers that persist QC output — the tile manifest inside an archive, the arrays beside an embedding — take their column names from here, so a renamed statistic moves every writer at once.

stats
stats() -> dict[str, float]

The three measured statistics as a mapping.

foreground_mask

foreground_mask(tile: ndarray, threshold: float = DEFAULT_THRESHOLDS.foreground_ratio) -> tuple[bool, float]

Is at least threshold percent of the tile non-background?

Returns (passes, foreground_ratio) — the ratio is a percentage, 0-100.

hsv_filter

hsv_filter(tile: ndarray, required_ratio: float = DEFAULT_THRESHOLDS.hsv_tissue_ratio) -> tuple[bool, float]

Is at least required_ratio percent of the tile stained tissue?

A pixel is stained when its saturation is above :data:SATURATION_MIN and its value below :data:VALUE_MAX; both bounds are exclusive.

Returns (passes, ratio) — the ratio is a percentage, 0-100.

blur_filter

blur_filter(tile: ndarray, blur_threshold: float = DEFAULT_THRESHOLDS.blur_laplacian_var) -> tuple[bool, float]

Is the tile sharp enough, by Laplacian variance?

Returns (passes, laplacian_var). The variance is a bare number, not a percentage: it scales with contrast, so a threshold tuned on one stain and scanner does not transfer unexamined to another.

qc_tile

qc_tile(tile: ndarray, thresholds: QcThresholds | None = None) -> QcResult

Run the three predicates over one tile and return the verdict.

The order is foreground, then blur, then stain, and it SHORT-CIRCUITS: a tile that fails an earlier predicate is not measured by the later ones, and their statistics come back nan. Two reasons, in this order:

  • cost — the cascade runs on every candidate tile of a slide, which is hundreds of thousands of them, and the cheapest predicate rejects the most;
  • meaning — the stain ratio of a tile that is 95 % empty glass is not a property of any tissue, and recording it as a number invites it into an average.

The order itself is part of the contract, because it decides which statistics exist: swapping blur and stain would leave a different column populated for exactly the same tiles.

Packing tiles

Build the patches archive a prediction takes, and validate one you have been handed. The container contract is on Observations.

pack

Build and validate the patches container.

A prediction takes one .zip per sample: the tiles as images, a tiles.csv naming each tile and where on the slide it came from, and optionally a thumbnail. It is a zip and not an array file because zip members stream — a loader can pull one tile at a time out of a 50k-tile archive, while a single packed array has to be materialised whole before the first tile is available.

Two directions, with very different obligations:

:func:pack_tiles produces an archive from tiles you already have. It runs quality control as the tiles stream past, writes only the ones that pass, strips every scrap of metadata from the images it writes, and reports what it dropped and why. Metadata stripping is not cosmetic: a tile cut from a scanned slide can inherit the slide label, the scanner serial and a capture timestamp, which is patient-adjacent data leaving the building inside an image nobody looks inside.

:func:read_archive consumes one, and treats it as hostile. Everything it can check from the archive's own index — member names, declared sizes, the manifest, the image headers — is checked BEFORE a single pixel is decoded, because a decoder is the largest attack surface in the whole path and the sizes a zip declares are written by whoever produced it.

PackError

Bases: ValueError

An archive cannot be built from what was passed.

ArchiveError

Bases: ValueError

An archive is not a valid container and must not be trusted.

Tile dataclass

One tile: an RGB uint8 image and its position on the slide.

x and y are the tile's top-left corner in full-resolution slide pixels, which is the frame every coordinate in the result is expressed in.

Rejection dataclass

One tile that quality control dropped, and the numbers behind it.

PackReport dataclass

What :func:pack_tiles wrote, and what it did not.

considered property
considered: int

Tiles handed in, whether they were written or not.

ArchiveRow dataclass

One row of an archive's manifest, parsed.

slots=True and no untyped spillover dict: an archive may declare 50k tiles and :class:PatchArchive holds every row for its whole open life, so a per-row dict of the same values as strings is tens of megabytes of exact duplicate. A reader that needs the extra QC columns should parse the manifest itself rather than have every reader pay for them.

PatchArchive

A validated archive, open for reading one tile at a time.

Constructed by :func:read_archive, which has already checked everything checkable without decoding. Decoding happens here, per tile, on demand.

The instance holds the archive OPEN, because a zip's index is read on open and re-opening per tile would make reading an archive quadratic in the number of tiles — which at 50k tiles is the difference between seconds and an afternoon. Close it when done, or use it as a context manager:

>>> with read_archive("sample.zip") as archive:   # doctest: +SKIP
...     for tile in archive.tiles():
...         ...
mpp property
mpp: float

The archive's resolution, which every row must agree on.

close
close() -> None

Release the archive's file handle.

read_tile
read_tile(name: str) -> np.ndarray

Decode one tile to an RGB uint8 array.

tiles
tiles() -> Iterator[Tile]

Every tile in manifest order, decoded one at a time.

thumbnail
thumbnail() -> np.ndarray | None

The overview image as RGB uint8, or None if there is none.

pack_tiles

pack_tiles(tiles: Iterable[Tile], out_zip: str | Path, *, mpp: float, thresholds: QcThresholds | None = None, thumbnail: ndarray | None = None) -> PackReport

Write the tiles that pass quality control into one archive.

Parameters:

Name Type Description Default
tiles Iterable[Tile]

Any iterable of :class:Tile. It is consumed lazily and one tile at a time, so a generator that decodes a slide region per step never holds more than one tile — which is what makes a whole slide packable on a laptop.

required
out_zip str | Path

Where to write. An existing file is replaced.

required
mpp float

Micrometres per pixel of the tiles, at the resolution they were cut. Recorded for every tile: without it a tile is a picture of unknown physical size and no model can be matched to it.

required
thresholds QcThresholds | None

Quality-control floors. The defaults if omitted.

None
thumbnail ndarray | None

An optional RGB overview image, written as the container's thumbnail. Any metadata it carries is dropped, as for every tile.

None

Returns:

Type Description
PackReport

Counts, plus one :class:Rejection per dropped tile naming the threshold that dropped it. A caller that gets an unexpectedly small archive can see immediately whether the slide was empty or a threshold was wrong.

read_archive

read_archive(zip_path: str | Path) -> PatchArchive

Validate an archive and return it, open for reading.

Every check a server has to make, in the order that keeps the risky ones last:

  1. it is a zip at all;
  2. every member is a plain file name at the root — no paths, no traversal;
  3. every DECLARED size, and their total, is inside the cap for this input kind — before anything is decompressed;
  4. the member names are exactly the manifest, the tiles matching the container's name pattern, and at most the thumbnail: a stray member is a refusal, not something to ignore;
  5. the manifest parses, starts with the agreed columns, and its rows and the tile members name each other exactly — in both directions;
  6. every tile's header agrees with its manifest row and with every other tile, so one sample is one geometry.

Raises :class:ArchiveError on the first failure, naming what failed.

Writing a result

The standard .h5ad, written with h5py alone and streamed batch by batch, so peak memory is one batch rather than the whole matrix.

h5ad

Write the standard result file with h5py alone.

The result of a prediction is an .h5ad: the expression matrix in X, one row per spot and one column per gene, with the per-spot table in obs, the per-gene table in var, the spot coordinates in obsm["spatial"], the run provenance in uns and any derived matrix in layers. It reads back as an ordinary AnnData.

Why this module does not import anndata. Two reasons, and the second is the load-bearing one:

  • anndata pulls pandas and its stack. This code runs inside GPU images whose compiled dependencies are ABI-pinned, and beside a model that wants the memory; the format is a handful of HDF5 attributes, so paying for that stack to write them is a poor trade.
  • a matrix bigger than memory cannot be handed to a library that takes an in-memory array. Accumulating a (300k x 19k) float32 matrix to write it in one call needs ~23 GB, which is simply not available — and the model that produced it is holding several GB of its own. So X is created empty, chunked and compressed, and filled batch by batch as the batches arrive: peak memory is one batch, whatever the slide.

The format is the on-disk contract anndata 0.10 and 0.11 read, expressed as encoding-type/encoding-version attributes on every element. The :func:write_result docstring names each one; the test suite holds the actual guarantee, by opening what this module writes with anndata itself.

ResultError

Bases: ValueError

The pieces handed to :func:write_result cannot form a valid result.

write_result

write_result(path: str | Path, *, obs: Mapping[str, Any] | None = None, var: Mapping[str, Any] | None = None, x: Any = None, obs_index: Sequence[Any] | None = None, var_index: Sequence[Any] | None = None, spatial: Any = None, uns: Mapping[str, Any] | None = None, layers: Mapping[str, Any] | None = None, obs_index_name: str = _DEFAULT_INDEX_NAME, var_index_name: str | None = None, dtype: Any = np.float32, compression: str | None = 'gzip') -> Path

Write one result file and return its path.

Parameters:

Name Type Description Default
path str | Path

Where to write. An existing file is replaced.

required
obs Mapping[str, Any] | None

The per-spot and per-gene tables, as mappings of column name to a one-dimensional sequence. Text columns are stored as text; everything else keeps its numpy dtype.

None
var Mapping[str, Any] | None

The per-spot and per-gene tables, as mappings of column name to a one-dimensional sequence. Text columns are stored as text; everything else keeps its numpy dtype.

None
x Any

The expression matrix: either a two-dimensional array, or an iterable of row batches covering every observation in order. The iterable form is what makes a matrix larger than memory writable — see :func:_write_matrix.

None
obs_index Sequence[Any] | None

Row and column names. Default to the positions as strings.

None
var_index Sequence[Any] | None

Row and column names. Default to the positions as strings.

None
var_index_name str | None

The name var's index carries in the file. Defaults to the gene identifier the shared result contract declares, so a reader knows what kind of identifier it is looking at without guessing.

None
spatial Any

An (n_obs, 2) array of spot coordinates, written to obsm["spatial"] — where every viewer looks for them.

None
uns Mapping[str, Any] | None

Free-form provenance, nested mappings allowed.

None
layers Mapping[str, Any] | None

Extra matrices of the same shape as x, each an array or an iterable of batches, written under layers/<name>.

None
dtype Any

The matrix dtype. float32 by default: float16 halves the file but is not enough precision for the statistics these matrices get fed to, and several tools refuse it outright.

float32
compression str | None

Passed to HDF5. "gzip" by default; None writes uncompressed.

'gzip'

Subsampling

When a slide yields more tiles than a run may spend, pick the densest contiguous square rather than a scatter — see Observations.

subsample

Spend a limited tile budget on the densest part of the slide.

A run is allowed a maximum number of tiles. When a slide yields more, something has to choose, and which tiles are dropped changes what the result can be used for: taking the first N walks a raster line across the slide, and taking a random N shreds the tissue into isolated spots, so every neighbourhood statistic downstream is computed over holes.

So the choice is a contiguous region — the densest square that holds the budget — and spatial structure survives inside it. What is lost is stated plainly: the tissue outside that square is not predicted at all, and a result must say so rather than look like a whole slide.

The implementation is an integral image: build an occupancy grid, prefix-sum it, then binary-search for the smallest square window that holds the budget and place it where the count is highest. That is O(n + G² log G) in the grid size, against the O(n² · iterations) of sweeping windows over the points themselves — the difference between milliseconds and minutes at 300k spots.

subsample_spatial_square

subsample_spatial_square(x: ndarray, y: ndarray, max_items: int) -> np.ndarray

Choose at most max_items of the given points, as a boolean mask.

Parameters:

Name Type Description Default
x ndarray

Coordinates of each candidate spot, in the same units (pixels), one entry per spot.

required
y ndarray

Coordinates of each candidate spot, in the same units (pixels), one entry per spot.

required
max_items int

The budget. When there are no more points than this, every point is kept and no work is done.

required

Returns:

Type Description
ndarray

A boolean mask over the input order: True for the points to keep. Returning a mask rather than the selected coordinates lets the caller apply the same choice to everything it holds per spot.

Contract values

The shared container layouts, caps and result layout, as data. Nothing in this package re-types a number the service also reads, and neither should your code.

contracts

The shared contract values, as data.

Container layouts, input-kind caps and the result layout are agreed between this package and the service that runs the same models. Re-typing one of those numbers here would create a second source for it, and the copy that drifts is always the one nobody is looking at — so the agreed values ship as the JSON they are defined in, vendored into the wheel at _contracts/.

The vendored copies are byte-for-byte what the definition side holds, and each one carries its sha256 in _contracts/MANIFEST.json. Only documents marked public are vendored; nothing else is readable from here, by construction.

>>> from auroraomics import contracts
>>> contracts.patch_archive()["manifest"]
'tiles.csv'

ContractError

Bases: RuntimeError

A vendored contract document is missing, unreadable or not itself.

manifest cached

manifest() -> dict[str, Any]

The vendored index: every document's relative name and its sha256.

names

names() -> tuple[str, ...]

Every vendored document name, sorted.

path_of

path_of(name: str) -> Path

The absolute path of one vendored document.

name must be one the index lists. Resolving an arbitrary string here would let a caller read any file the process can reach through a ../.. name, so the index is the allow-list rather than a hint.

load cached

load(name: str) -> dict[str, Any]

One vendored document, parsed. Cached: these are read-only constants.

verify

verify() -> None

Re-hash every vendored document and compare with the index.

Raises :class:ContractError on the first mismatch. Not called on import — hashing on every process start would tax every caller for a check that only a damaged install can fail — but cheap enough to call from a diagnostic.

qc_thresholds

qc_thresholds() -> dict[str, float]

The three patch-QC floors the service runs.

Read from the shipped contract rather than written here: a client that filters tiles differently from the server it sends them to would drop tiles the service would have kept, or keep tiles it will discard, and every number downstream would still look plausible.

foreground_white_level

foreground_white_level() -> int

The grey level at or below which a pixel counts as foreground.

Read from the contract for the same reason :func:qc_thresholds is: this is the other half of the foreground rule, and a client that cuts foreground at a different grey level than the service does filters a different set of tiles — while every number downstream still looks plausible.

patch_archive

patch_archive() -> dict[str, Any]

The patches container layout: manifest name, columns, tile names.

result_layout

result_layout() -> dict[str, Any]

The result file's agreed layout: format, var index, layer names.

input_kind

input_kind(kind: str) -> dict[str, Any]

One input kind's registry row (container, cost unit, caps).

max_bytes

max_bytes(kind: str) -> int

The largest an input of kind may be, uncompressed.

A cap is what makes it safe to look at an archive at all: the sizes a zip declares are written by whoever produced it, so they are checked against this number before anything is decompressed.