Query arrays in storage
¶
This guide covers streaming array-like datasets — AnnData, SpatialData, and generic HDF5 — directly from disk or cloud storage. For tabular datasets, see Query tables in storage .
# replace with your username and S3 bucket
lamin login testuser1
lamin init --storage s3://lamindb-ci/test-arrays
Show code cell output
→ logged in testuser1
! updating cloud SQLite 's3://lamindb-ci/test-arrays/.lamindb/lamin.db' of instance 'testus
er1/test-arrays'
! locked instance (to unlock and push changes to the cloud SQLite file, call: lamin disconn
ect)
→ initialized lamindb: testuser1/test-arrays
Import lamindb and track this notebook.
import lamindb as ln
import numpy as np
db = ln.DB("laminlabs/lamindata") # we'll pull the SpatialData example from there
Show code cell output
→ connected lamindb: testuser1/test-arrays
! database has modules bionty,pertdb, configure it: lamin settings modules set bionty,pertdb
AnnData¶
We’ll need some test data:
ln.Artifact("s3://lamindb-ci/test-arrays/pbmc68k.h5ad").save()
ln.Artifact("s3://lamindb-ci/test-arrays/testfile.hdf5").save()
Show code cell output
! no run & transform got linked, call `ln.track()` & re-run
! no run & transform got linked, call `ln.track()` & re-run
Artifact(uid='ulAQTAxc4aBHyFW80000', key='testfile.hdf5', description=None, suffix='.hdf5', kind=None, otype=None, size=1400, hash='UCWPjJkhzBjO97rtuo_8Yg', n_files=None, n_observations=None, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=None, schema_id=None, created_by_id=1, created_at=2026-07-17 13:46:59 UTC, is_locked=False, version_tag=None, is_latest=True)
An h5ad artifact stored on s3:
artifact = ln.Artifact.get(key="pbmc68k.h5ad")
artifact.path
Show code cell output
S3QueryPath('lamindb-ci/test-arrays/pbmc68k.h5ad', protocol='s3')
adata = artifact.open()
Show code cell output
! run input wasn't tracked, call `ln.track()` and re-run
This is an AnnDataAccessor — an AnnData backed by cloud storage:
adata
Show code cell output
AnnDataAccessor object with n_obs × n_vars = 70 × 765
constructed for the AnnData object pbmc68k.h5ad
obs: ['cell_type', 'index', 'louvain', 'n_genes', 'percent_mito']
obsm: ['X_pca', 'X_umap']
obsp: ['connectivities', 'distances']
uns: ['louvain', 'louvain_colors', 'neighbors', 'pca']
var: ['highly_variable', 'index', 'n_counts']
varm: ['PCs']
Without subsetting, it references the underlying lazy h5 or zarr arrays:
adata.X
Show code cell output
<HDF5 dataset "X": shape (70, 765), type "<f4">
You can subset it like a normal AnnData object:
obs_idx = adata.obs.cell_type.isin(["Dendritic cells", "CD14+ Monocytes"]) & (
adata.obs.percent_mito <= 0.05
)
adata_subset = adata[obs_idx]
adata_subset
Show code cell output
AnnDataAccessorSubset object with n_obs × n_vars = 35 × 765
obs: ['cell_type', 'index', 'louvain', 'n_genes', 'percent_mito']
obsm: ['X_pca', 'X_umap']
obsp: ['connectivities', 'distances']
uns: ['louvain', 'louvain_colors', 'neighbors', 'pca']
var: ['highly_variable', 'index', 'n_counts']
varm: ['PCs']
Subsets load arrays into memory upon direct access:
adata_subset.X
Show code cell output
array([[-0.326, -0.191, 0.499, ..., -0.21 , -0.636, -0.49 ],
[ 0.811, -0.191, -0.728, ..., -0.21 , 0.604, -0.49 ],
[-0.326, -0.191, 0.643, ..., -0.21 , 2.303, -0.49 ],
...,
[-0.326, -0.191, -0.728, ..., -0.21 , 0.626, -0.49 ],
[-0.326, -0.191, -0.728, ..., -0.21 , -0.636, -0.49 ],
[-0.326, -0.191, -0.728, ..., -0.21 , -0.636, -0.49 ]],
shape=(35, 765), dtype=float32)
To load the entire subset into memory as an actual AnnData object, use to_memory():
adata_subset.to_memory()
Show code cell output
AnnData object with n_obs × n_vars = 35 × 765
obs: 'cell_type', 'n_genes', 'percent_mito', 'louvain'
var: 'n_counts', 'highly_variable'
uns: 'louvain', 'louvain_colors', 'neighbors', 'pca'
obsm: 'X_pca', 'X_umap'
varm: 'PCs'
obsp: 'connectivities', 'distances'
You can also add columns to .obs and .var of a cloud AnnData without downloading it. First, create a new AnnData zarr artifact:
adata_subset.to_memory().write_zarr("adata_subset.zarr")
artifact = ln.Artifact(
"adata_subset.zarr", description="test add column to adata"
).save()
Show code cell output
! no run & transform got linked, call `ln.track()` & re-run
This is how you add a column:
with artifact.open(mode="r+") as adata_accessor:
adata_accessor.add_column(where="obs", col_name="ones", col=np.ones(adata_accessor.shape[0]))
display(adata_accessor)
Show code cell output
! run input wasn't tracked, call `ln.track()` and re-run
AnnDataAccessor object with n_obs × n_vars = 35 × 765
constructed for the AnnData object 9EtBNxRhyxNeS0sj.zarr
obs: ['cell_type', 'index', 'louvain', 'n_genes', 'percent_mito', 'ones']
obsm: ['X_pca', 'X_umap']
obsp: ['connectivities', 'distances']
uns: ['louvain', 'louvain_colors', 'neighbors', 'pca']
var: ['highly_variable', 'index', 'n_counts']
varm: ['PCs']
! no run & transform got linked, call `ln.track()` & re-run
The version of the artifact is updated after the modification.
artifact
Show code cell output
Artifact(uid='9EtBNxRhyxNeS0sj0001', key=None, description='test add column to adata', suffix='.zarr', kind=None, otype=None, size=215962, hash='r4R-x3hgQVsA_0LUE2p1wQ', n_files=123, n_observations=None, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=None, schema_id=None, created_by_id=1, created_at=2026-07-17 13:47:07 UTC, is_locked=False, version_tag=None, is_latest=True)
artifact.delete(permanent=True)
Show code cell output
→ deleting all versions of this artifact because they all share the same store
SpatialData¶
You can also access AnnData objects inside SpatialData tables:
artifact = db.Artifact.get(key="visium_aligned_guide_min.zarr")
access = artifact.open()
Show code cell output
! run input wasn't tracked, call `ln.track()` and re-run
access
Show code cell output
SpatialDataAccessor object
constructed for the SpatialData object bjH534dxVi1drmLZ.zarr
with tables: ['table']
access.tables
Show code cell output
Accessor for the SpatialData attribute tables
with keys: ['table']
This gives you the same AnnDataAccessor object as for a normal AnnData.
table = access.tables["table"]
table
Show code cell output
AnnDataAccessor object with n_obs × n_vars = 37 × 18085
constructed for the AnnData object table
obs: ['_index', 'array_col', 'array_row', 'clone', 'dataset', 'in_tissue', 'region', 'spot_id']
obsm: ['spatial']
uns: ['spatial', 'spatialdata_attrs']
var: ['feature_types', 'gene_ids', 'genome', 'symbols']
You can subset it and read into memory as an actual AnnData:
table_subset = table[table.obs["clone"] == "diploid"]
table_subset
Show code cell output
AnnDataAccessorSubset object with n_obs × n_vars = 31 × 18085
obs: ['_index', 'array_col', 'array_row', 'clone', 'dataset', 'in_tissue', 'region', 'spot_id']
obsm: ['spatial']
uns: ['spatial', 'spatialdata_attrs']
var: ['feature_types', 'gene_ids', 'genome', 'symbols']
adata = table_subset.to_memory()
Generic HDF5¶
Let us query a generic HDF5 artifact:
artifact = ln.Artifact.get(key="testfile.hdf5")
And get a backed accessor:
backed = artifact.open()
Show code cell output
! run input wasn't tracked, call `ln.track()` and re-run
The returned object contains the .connection and h5py.File or zarr.Group in .storage
backed
Show code cell output
BackedAccessor(connection=<File-like object S3FileSystem, lamindb-ci/test-arrays/testfile.hdf5>, storage=<HDF5 file "testfile.hdf5>" (mode r)>)
backed.storage
Show code cell output
<HDF5 file "testfile.hdf5>" (mode r)>
Show code cell content
lamin delete --force test-arrays
→ deleted storage record on hub 76e5f3b018085f52bcd5ca9b4d7e0ce5 | s3://lamindb-ci/test-a
rrays
→ deleted instance record on hub 587a82023ecb5ea28b3a448cb8240f7f