Quickstart¶
Fifteen minutes, no account, no network: prepare a patches archive from tiles,
then write and read the standard result file. This is the whole local path, and
it is the part of the system that works today.
Not on the package index yet
The first release is imminent, so the install line below will not find it for a little longer. Every example on this page is executed against the package on every build of this site, so what you read here runs.
The hosted API is not open yet
Nothing on this page talks to a server, and nothing on this page uploads a pixel. Submitting to the hosted API needs a key, and keys are not being minted.
Platform status: https://auroraomics.org/api/health. Which runtimes are accepting work is reported by GET / on the API itself — its lanes list is empty while none is open, and the API host does not answer at all before launch.
Install¶
| Import as | auroraomics |
| Python | >=3.10 |
| Licence | PolyForm-Noncommercial-1.0.0 |
The licence is PolyForm-Noncommercial-1.0.0 — non-commercial use only. Research at a non-profit institution is squarely inside it; using it in or for a business is not, whatever the output is used for. Read it before you build on it: https://polyformproject.org/licenses/noncommercial/1.0.0. Model weights carry their own separate terms, which the model's card names.
The core needs only NumPy, h5py, OpenCV and Pillow — no deep-learning framework, no slide reader, nothing that has to be compiled. Everything heavier is an extra, so packing tiles never makes you install a runtime you will not run:
pip install "auroraomics[client]" # the hosted API client
pip install "auroraomics[deepspotm]" # running a model locally — reserved; installs nothing yet
pip install "auroraomics[slide]" # reading pyramidal slide formats directly
1. Tiles, and which of them are worth predicting on¶
A model does not see your slide; it sees square tiles cut from it at a known physical size. Three quality tests decide which tiles are worth spending a prediction on: how much of the tile is foreground, how much of it is stained tissue, and whether it is in focus. They run on every tile, and you can run them yourself on one:
import numpy as np
from auroraomics import Tile, contracts, qc_tile
# Stand-in tiles, so this page runs anywhere. In practice these come from your
# slide — see step 4.
rng = np.random.default_rng(0)
def stand_in_tile(size: int = 120) -> np.ndarray:
"""A square of plausibly stained tissue: pink-purple, with texture."""
base = np.array([196, 132, 178], dtype=np.int16)
return np.clip(base + rng.integers(-28, 28, size=(size, size, 3)), 0, 255).astype(np.uint8)
print("quality floors:", contracts.qc_thresholds())
verdict = qc_tile(stand_in_tile())
print("passes:", verdict.passes, "| rejected by:", verdict.rejected_by)
print("measured:", verdict.stats())
qc_tile returns the three measurements as well as the verdict, so a tile that
was dropped can tell you which floor it missed instead of just disappearing.
2. Pack the tiles that pass¶
pack_tiles streams an iterable of tiles, quality-checks each one, writes the
survivors into a single archive with a manifest, and hands back a report.
from auroraomics import pack_tiles
tiles = [
Tile(image=stand_in_tile(), x=(i % 4) * 120, y=(i // 4) * 120)
for i in range(16)
]
report = pack_tiles(tiles, "sample.zip", mpp=0.499)
print(report.written, "tiles kept,", report.rejected, "dropped")
print("dropped by:", report.rejected_by_reason)
Two things about that call matter more than they look.
mpp is micrometres per pixel at the resolution the tiles were cut. Without
it a tile is a picture of unknown physical size, and no model can be matched to
it — every model states the physical patch edge it was trained on, in its
input_spec.
The iterable is consumed lazily, one tile at a time. A generator that decodes one slide region per step never holds more than one tile, which is what makes a whole slide packable on a laptop.
3. Read an archive back before you trust it¶
read_archive validates member names, the manifest, the tile geometry and the
declared sizes before decoding a single pixel, then decodes tiles on demand.
from auroraomics import read_archive
with read_archive("sample.zip") as archive:
print(len(archive), "tiles at", archive.mpp, "µm/px")
first = next(archive.tiles())
print("first tile:", first.image.shape, "at", (first.x, first.y))
That archive is exactly what a patches submission carries — see
Observations for the container contract, and
Limits for how large it may be.
4. From a real slide¶
Reading a pyramidal slide needs the slide extra. The shape of the loop is the
point: cut a region, wrap it in a Tile with its full-resolution position, and
yield it. Nothing accumulates.
import tifffile
from auroraomics import Tile, pack_tiles
def tiles_from(path, edge=120):
with tifffile.TiffFile(path) as slide:
page = slide.pages[0]
image = page.asarray()
height, width = image.shape[:2]
for y in range(0, height - edge, edge):
for x in range(0, width - edge, edge):
yield Tile(image=image[y : y + edge, x : x + edge], x=x, y=y)
report = pack_tiles(tiles_from("slide.svs"), "sample.zip", mpp=0.499)
Read the slide's own mpp from its metadata rather than assuming one. A tile
cut at a different resolution from the one you declare is the single most
common way to get a confident, wrong prediction.
5. The result file¶
Every prediction — whichever runtime produced it — is one .h5ad with the same
layout, and this package writes it. You will mostly read these files, but
writing one is the fastest way to see the layout, and it is how you would store
a prediction you made yourself:
import h5py
from auroraomics import write_result
genes = ["ENSG00000121410", "ENSG00000148584", "ENSG00000175899"]
symbols = ["A1BG", "A1CF", "A2M"]
spots = 16
path = write_result(
"prediction.h5ad",
x=rng.random((spots, len(genes)), dtype=np.float32),
obs={"tile_id": [f"tile_{i:04d}" for i in range(spots)]},
var={"feature_name": symbols},
var_index=genes,
spatial=np.array([[(i % 4) * 120, (i // 4) * 120] for i in range(spots)]),
uns={"note": "random numbers — this is a layout demonstration"},
)
with h5py.File(path) as handle:
index_name = handle["var"].attrs["_index"]
print("matrix:", handle["X"].shape, handle["X"].dtype)
print("gene axis indexed by:", index_name)
print("coordinates:", handle["obsm/spatial"].shape)
assert index_name == contracts.result_layout()["var_index"]
The gene axis is indexed by Ensembl stable identifier and the symbol travels beside it, because symbols are renamed and reused and identifiers are not. See Genes.
x may also be an iterable of row batches rather than an array. Each batch
is compressed into the file as it arrives and then dropped, so a matrix larger
than memory is writable — which is what a whole slide against a full gene panel
is.
The file reads back as an ordinary AnnData:
Next¶
- A submission — what the hosted API takes, and in which slots.
input_spec— how a model states what it needs.- Limits — the caps, before you meet them as an error.
- Privacy and retention — what is stored when you do submit, and for how long.