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
¶
The threshold keys, in declaration order.
from_input_spec
classmethod
¶
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 ¶
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.
stat_names
classmethod
¶
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.
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 ¶
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.
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():
... ...
thumbnail ¶
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: |
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: |
read_archive ¶
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:
- it is a zip at all;
- every member is a plain file name at the root — no paths, no traversal;
- every DECLARED size, and their total, is inside the cap for this input kind — before anything is decompressed;
- 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;
- the manifest parses, starts with the agreed columns, and its rows and the tile members name each other exactly — in both directions;
- 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
Xis 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: |
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 |
None
|
spatial
|
Any
|
An |
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 |
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'
|
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 ¶
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: |
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
¶
The vendored index: every document's relative name and its sha256.
path_of ¶
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
¶
One vendored document, parsed. Cached: these are read-only constants.
verify ¶
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 ¶
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 ¶
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 ¶
The patches container layout: manifest name, columns, tile names.
result_layout ¶
The result file's agreed layout: format, var index, layer names.
input_kind ¶
One input kind's registry row (container, cost unit, caps).
max_bytes ¶
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.