Persistence and interchange¶
ALHENA results can be saved in several formats depending on whether you need only the embedding matrix, the complete analysis table, or an integrated spatial object. The formats below preserve different amounts of metadata and spatial information.
Tile embeddings¶
| Format | Holds | Written by | Read by |
|---|---|---|---|
.h5ad |
the full table: embeddings, identifiers, spatial bounds and centers when available, and provenance | tiles.write_h5ad(path); alhena encode --out tiles.h5ad |
io.read_tile_embeddings(path) |
| SpatialData | the tile table together with one spatial rectangle per tile | spatial.tiles_to_spatialdata(sdata, tiles); tiles.to_spatialdata() |
io.read_tile_embeddings(sdata.tables[key]) |
.h5, .hdf5 |
the matrix, the tile names, and the geometry of the run | alhena encode --out tiles.h5 |
h5py |
.npy |
the matrix alone, with the geometry of the run in a .geometry.json file beside it |
alhena encode --out tiles.npy |
NumPy |
.h5ad is the analysis-ready form, and the one to use by default:
tiles.write_h5ad("tiles.h5ad")
tiles = alhena.io.read_tile_embeddings("tiles.h5ad")
print(tiles.purpose, tiles.bounds_frame, tiles.mpp)
read_tile_embeddings() reconstructs the TileEmbeddings object, including spatial bounds,
purpose, and encoding geometry when these were stored. The lighter .h5 and .npy formats
preserve the embedding matrix and geometry information but not the full spatial metadata.
alhena encode lists what each tile-output format preserves.
A .h5 file holds two datasets, embeddings, one row per tile, and barcode, the name of each
tile. The geometry of the run is stored as attributes of the file: encoder_id, the fields of
ResampleInfo and embedding_shape, with NaN for a field that has no value. The
.geometry.json file beside a .npy holds the same record, with null for a missing value.
Cell embeddings and annotations¶
| Result | Format | Holds | Written by | Read by |
|---|---|---|---|---|
CellEmbeddings |
.h5ad |
both embeddings, the positions, the provenance | cells.write_h5ad(path); alhena encode --nuclei ... --out cells.h5ad |
io.read_cells(path) |
CellEmbeddings |
SpatialData | the .h5ad table, annotating the element the nuclei came from |
spatial.cells_to_spatialdata(sdata, cells, region=...); cells.to_spatialdata(sdata, region=...) |
io.read_cells(table) |
AnnotationResult |
.h5ad |
one or several annotations, the cells they were made from, and their provenance | result.write_h5ad(path); io.add_annotation(table, result) for each further annotation; alhena annotate --out annotations.h5ad |
io.read_annotations(path, name=...) for one annotation with its cells; io.read_cells(path) for the cells alone |
AnnotationResult |
.parquet, .csv |
the annotations alone, laid out as to_frame() lays them |
result.to_frame(); alhena annotate --out annotations.parquet |
pandas |
AnnotationResult |
SpatialData | the .h5ad table, annotating the element the nuclei came from |
spatial.cells_to_spatialdata(sdata, result, region=...) or result.to_spatialdata(sdata, region=...), once per annotation |
io.read_annotations(table, name=...); io.read_cells(table) for the cells alone |
Save an AnnotationResult as .h5ad when the underlying cell embeddings may need to be reused.
io.read_annotations() reads an annotation back from this file, with its cells, so it can be
drawn or evaluated again; name= chooses one where the file holds several. io.read_cells()
recovers the CellEmbeddings alone, allowing them to be annotated again without another
image-encoder pass.
A table records the identifier and the position of each cell, not the outline of its nucleus. The
outlines stay in the segmentation they were read from, and the identifiers link each row to its
nucleus there. spatial.cells_to_spatialdata() keeps the table and the nuclei together in one
SpatialData object. .parquet and .csv exports contain the annotation
table only and do not preserve the embeddings or full provenance.
Several annotations of the same cells¶
One table holds the cells once, with their embeddings, and any number of annotations of them.
Each annotation adds its own fields, named after the annotation. The name is the axis unless
another is given, so annotations on lineage_v1 and on compartment_v1 share one table:
from alhena import io
table = lineage.to_anndata() # the cells, and alhena_celltype_lineage_v1
io.add_annotation(table, broad) # adds alhena_celltype_compartment_v1
table.write_h5ad("annotations.h5ad")
Here lineage and broad are the results of annotating the same CellEmbeddings on the
two axes. Annotations on one axis against two references take a name each, as
io.add_annotation(table, result, name="lineage_v1_second").
add_annotation() refuses an annotation whose name the table already holds, unless
overwrite=True, which replaces that annotation alone. It also refuses annotations of other
cells: identifiers that differ from those of the table, in its order, or another cell space
identity. To add an annotation to a saved table, read it with anndata.read_h5ad(), add the
annotation, and write the table back.
The table layouts¶
ALHENA stores embeddings in AnnData obsm, while obs contains per-tile or per-cell metadata and
uns stores provenance. X is intentionally empty because the embedding dimensions are
representation features rather than measured variables. Every key below is a constant of
alhena.io, or for the fields of an annotation a constant followed by the annotation name, so
code reading a table need not spell one out.
Tiles¶
| Where | Key | Constant | Holds |
|---|---|---|---|
obs index |
the name of each tile | ||
obs |
tile_id |
TILE_ID_COLUMN |
the same names, as a column |
obs |
alhena_tile_index |
TILE_INDEX_COLUMN |
the position of each tile among the tiles given |
obs |
alhena_tile_x0, alhena_tile_y0, alhena_tile_x1, alhena_tile_y1 |
TILE_BOUNDS_COLUMNS |
tile bounds (x0, y0, x1, y1), upper bounds excluded |
obsm |
X_alhena_tile |
TILE_EMBEDDING_KEY |
the embeddings, (n_tiles, 1536) float32 |
obsm |
spatial |
SPATIAL_KEY |
center of each tile's spatial bounds, (x, y) |
uns |
alhena_tiles |
TILES_PROVENANCE_KEY |
the provenance: encoder, UNI2-h model weights, purpose, geometry, frame, microns per unit, image shape |
The bound columns and obsm["spatial"] are absent when the tiles have no shared coordinate frame.
Cells¶
| Where | Key | Constant | Holds |
|---|---|---|---|
obs index |
each identifier, as text | ||
obs |
cell_id |
CELL_ID_COLUMN |
the identifiers, in the type they were given: integer or text |
obs |
alhena_tile_index |
TILE_INDEX_COLUMN |
the tile each cell was read from |
obs |
alhena_nucleus_px |
NUCLEUS_PX_COLUMN |
the nucleus pixels supporting it there |
obs |
alhena_n_candidate_tiles |
CANDIDATE_TILES_COLUMN |
how many tiles held the nucleus |
obs |
alhena_embedding_valid |
EMBEDDING_VALID_COLUMN |
whether the embedding may be annotated |
obsm |
X_alhena_backbone |
BACKBONE_KEY |
the pre-projection cell embeddings, (n_cells, 1536) float32 |
obsm |
X_alhena_cell |
CELL_SPACE_KEY |
projected embeddings in the shared cell space, (n_cells, 512) float32 |
obsm |
spatial |
SPATIAL_KEY |
the position of each nucleus, x then y, where recorded |
uns |
alhena_cells |
CELLS_PROVENANCE_KEY |
the provenance of the cells |
The cell provenance records the encoder and cell-space identities, the digest of the UNI2-h model
weights and the geometry of the run. It also records where the nuclei came from, whether their
shapes were approximated, how many nuclei reached no token, the frame and microns per unit of
spatial, and the identifier type.
Annotations¶
Each annotation adds the following fields to the cell table. <name> is the name of the
annotation: its axis unless another name was given. The annotation on the default axis therefore
lands in alhena_celltype_lineage_v1.
| Where | Key | Constant | Holds |
|---|---|---|---|
obs |
alhena_celltype_<name> |
LABEL_COLUMN |
predicted cell type; "" when unassigned |
obs |
alhena_confidence_<name> |
CONFIDENCE_COLUMN |
the winning class's share of the vote |
obs |
alhena_valid_<name> |
VALID_COLUMN |
whether an annotation was assigned |
obs |
alhena_class_entropy_<name> |
CLASS_ENTROPY_COLUMN |
the entropy of the class mass |
obs |
alhena_neighbor_entropy_<name> |
NEIGHBOR_ENTROPY_COLUMN |
the entropy of the neighbor weights |
obsm |
alhena_mass_<name> |
MASS_KEY |
the class mass, in class order |
uns |
alhena_celltype_<name>_colors |
the color of each cell type in the default palette, as Scanpy reads it; absent where the palette lacks one of the cell types | |
uns |
alhena, then annotations, then <name> |
PROVENANCE_KEY, ANNOTATIONS_KEY |
the provenance of the annotation: reference, axis, class order, k, tau, cell-space identity |
A table holding no annotation has no alhena entry in uns.
A provenance entry whose value is unknown is omitted because these file formats have no null
representation for it. read_cells and read_tile_embeddings read such a missing entry as
unknown, and refuse a table lacking an entry every writer records.
Writing into a SpatialData object¶
A SpatialData object can store the H&E image, nuclei, tile regions, and ALHENA result tables while
preserving their coordinate transformations. This functionality requires the spatial extra.
Tiles. spatial.tiles_to_spatialdata adds one rectangle per tile, and the tile table:
With image_key, the tile rectangles inherit the transformations of the selected image and remain
registered with it. Tiles that spatial.tile_source() read from an image of the object inherit
its transformations without image_key. Other tiles without image_key are added in the global
coordinate system with an identity transformation.
tiles.to_spatialdata() provides the object-oriented equivalent, and can also create a new
SpatialData object containing only the tile regions and tile table when no existing object is
supplied. By default, the shapes and table are stored as alhena_tiles and
alhena_tile_embeddings.
Cells and their annotations. spatial.cells_to_spatialdata() adds the table of cells to an
existing SpatialData object and associates each row with the nucleus element supplied through
region=. The first call writes the cells, alone or with an annotation. Each later call adds the
annotations of the same cells to that table:
spatial.cells_to_spatialdata(sdata, lineage, region="nucleus_boundaries")
spatial.cells_to_spatialdata(sdata, broad, region="nucleus_boundaries")
cells.to_spatialdata(sdata, region=...) and result.to_spatialdata(sdata, region=...) are the
object-oriented equivalents.
The resulting table contains the same ALHENA information as the .h5ad representation, together
with the SpatialData region information required to link each row to its nucleus. Cell
identifiers that do not match the selected nucleus element are refused, and nothing is written.
Each annotation is named after its axis unless name= is given, and annotations of other cells
are refused, as Several annotations of the same cells
describes. The table is stored as alhena_cells unless another name is specified.
Saving. Use sdata.write(path) to write a new SpatialData object. If the object was opened from
an existing store and only a newly added element needs to be persisted, write that element
explicitly:
After an annotation is added to a table already in the store, write the table again.
write_element() does not overwrite an element of the store the object was read from, so delete
the copy on disk first. The table is held in memory, so nothing is lost:
Moving results between environments¶
The .h5ad files written by ALHENA use standard AnnData storage and can be transferred between
compatible Python environments. This allows, for example, cell embeddings to be computed on one
machine and annotated later on another without rerunning the image encoder.
For the supported package versions, see Installation.
Next: Tutorials