Multi-modal
¶
Here, we’ll showcase how to curate and register ECCITE-seq data from Papalexi21 in the form of MuData objects.
ECCITE-seq is designed to enable interrogation of single-cell transcriptomes together with surface protein markers in the context of CRISPR screens.
MuData objects build on top of AnnData objects to store multimodal data.
# pip install lamindb
lamin init --storage ./test-multimodal --modules bionty
Show code cell output
→ set dev-dir: /home/runner/work/lamin-usecases/lamin-usecases/docs
→ initialized lamindb: testuser1/test-multimodal
import lamindb as ln
import bionty as bt
bt.settings.organism = "human"
ln.track()
Show code cell output
→ connected lamindb: testuser1/test-multimodal
→ created Transform('XuA4SbIcW5MN0000', key='multimodal.ipynb'), started new Run('3iq1t42PFkqQCHac') at 2026-08-25 14:24:29 UTC
→ notebook imports: bionty==2.4.3 lamindb-core==2.9.1
• tip: to identify the notebook across renames, pass the uid: ln.track("XuA4SbIcW5MN")
Creating MuData Artifacts¶
lamindb provides a from_mudata() method to create Artifact from MuData objects.
mdata = ln.core.datasets.mudata_papalexi21_subset()
mdata
Show code cell output
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/mudata/_core/mudata.py:1354: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
mod_df.rename(
MuData object with n_obs × n_vars = 200 × 300
obs: 'perturbation', 'replicate'
var: 'name'
4 modalities
rna: 200 × 173
obs: 'nCount_RNA', 'nFeature_RNA', 'percent.mito'
var: 'name'
adt: 200 × 4
obs: 'nCount_ADT', 'nFeature_ADT'
var: 'name'
hto: 200 × 12
obs: 'nCount_HTO', 'nFeature_HTO', 'technique'
var: 'name'
gdo: 200 × 111
obs: 'nCount_GDO'
var: 'name'
mdata_artifact = ln.Artifact.from_mudata(mdata, key="papalexi.h5mu")
mdata_artifact
Show code cell output
Artifact(uid='paxsRZIjEe5PHX7v0000', key='papalexi.h5mu', description=None, suffix='.h5mu', kind='dataset', otype='MuData', size=549976, hash='yOTamE22ANM-Ph1iqxB7Gw', n_files=None, n_observations=200, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=1, schema_id=None, created_by_id=1, created_at=<django.db.models.expressions.DatabaseDefault object at 0x7fe4b4435df0>, is_locked=False, version_tag=None, is_latest=True)
# MuData Artifacts have the corresponding otype
mdata_artifact.otype
Show code cell output
'MuData'
# MuData Artifacts can easily be loaded back into memory
papalexi_in_memory = mdata_artifact.load()
papalexi_in_memory
Show code cell output
MuData object with n_obs × n_vars = 200 × 300
obs: 'perturbation', 'replicate'
var: 'name'
4 modalities
rna: 200 × 173
obs: 'nCount_RNA', 'nFeature_RNA', 'percent.mito'
var: 'name'
adt: 200 × 4
obs: 'nCount_ADT', 'nFeature_ADT'
var: 'name'
hto: 200 × 12
obs: 'nCount_HTO', 'nFeature_HTO', 'technique'
var: 'name'
gdo: 200 × 111
obs: 'nCount_GDO'
var: 'name'
Schema¶
# define labels
perturbation = ln.ULabel(name="Perturbation", is_type=True).save()
ln.ULabel(name="Perturbed", type=perturbation).save()
ln.ULabel(name="NT", type=perturbation).save()
replicate = ln.ULabel(name="Replicate", is_type=True).save()
ln.ULabel(name="rep1", type=replicate).save()
ln.ULabel(name="rep2", type=replicate).save()
ln.ULabel(name="rep3", type=replicate).save()
# define obs schema
obs_schema = ln.Schema(
name="mudata_papalexi21_subset_obs_schema",
features=[
ln.Feature(name="perturbation", dtype=perturbation).save(),
ln.Feature(name="replicate", dtype=replicate).save(),
],
).save()
obs_schema_rna = ln.Schema(
name="mudata_papalexi21_subset_rna_obs_schema",
features=[
ln.Feature(name="nCount_RNA", dtype=int).save(),
ln.Feature(name="nFeature_RNA", dtype=int).save(),
ln.Feature(name="percent.mito", dtype=float).save(),
],
coerce_dtype=True,
).save()
obs_schema_hto = ln.Schema(
name="mudata_papalexi21_subset_hto_obs_schema",
features=[
ln.Feature(name="nCount_HTO", dtype=int).save(),
ln.Feature(name="nFeature_HTO", dtype=int).save(),
ln.Feature(name="technique", dtype=bt.ExperimentalFactor).save(),
],
coerce_dtype=True,
).save()
var_schema_rna = ln.Schema(
name="mudata_papalexi21_subset_rna_var_schema",
itype=bt.Gene.symbol,
dtype=float,
).save()
# define composite schema
mudata_schema = ln.Schema(
name="mudata_papalexi21_subset_mudata_schema",
otype="MuData",
slots={
"obs": obs_schema,
"rna:obs": obs_schema_rna,
"hto:obs": obs_schema_hto,
"rna:var": var_schema_rna,
},
).save()
Show code cell output
! tip: pass `type` to map ulabel into a type hierarchy
! tip: pass `type` to map ulabel into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map schema into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map schema into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! you are trying to create a record with name='nFeature_HTO' but a record with similar name exists: 'nFeature_RNA'. Did you mean to load it?
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map schema into a type hierarchy
! tip: pass `type` to map schema into a type hierarchy
! tip: pass `type` to map schema into a type hierarchy
/tmp/ipykernel_3757/897646117.py:20: DeprecationWarning: `coerce_dtype` argument was renamed to `coerce` and will be removed in a future release.
obs_schema_rna = ln.Schema(
/tmp/ipykernel_3757/897646117.py:30: DeprecationWarning: `coerce_dtype` argument was renamed to `coerce` and will be removed in a future release.
obs_schema_hto = ln.Schema(
mudata_schema.describe()
Show code cell output
Schema: mudata_papalexi21_subset_mudata_schema ├── uid: Nc47LQHOsbPRybeC run: 3iq1t42 (multimodal.ipynb) │ otype: MuData created_at: 2026-08-25 14:24:31 UTC │ created_by: testuser1 ├── obs: mudata_papalexi21_subset_obs_schema │ ├── uid: VfdqJgALmYij3GyJ run: 3iq1t42 (multimodal.ipynb) │ │ created_at: 2026-08-25 14:24:31 UTC created_by: testuser1 │ └── Features (2) │ └── name dtype optional nullable coerce default_value │ perturbation ULabel[Perturbation] ✗ ✓ ✗ unset │ replicate ULabel[Replicate] ✗ ✓ ✗ unset ├── rna:obs: mudata_papalexi21_subset_rna_obs_schema │ ├── uid: JXUP2jIda3cRslcl run: 3iq1t42 (multimodal.ipynb) │ │ created_at: 2026-08-25 14:24:31 UTC created_by: testuser1 │ └── Features (3) │ └── name dtype optional nullable coerce default_value │ nCount_RNA int ✗ ✓ ✓ unset │ nFeature_RNA int ✗ ✓ ✓ unset │ percent.mito float ✗ ✓ ✓ unset ├── hto:obs: mudata_papalexi21_subset_hto_obs_schema │ ├── uid: 8c6hV7pDUAAhXfXf run: 3iq1t42 (multimodal.ipynb) │ │ created_at: 2026-08-25 14:24:31 UTC created_by: testuser1 │ └── Features (3) │ └── name dtype optional nullable coerce default_value │ nCount_HTO int ✗ ✓ ✓ unset │ nFeature_HTO int ✗ ✓ ✓ unset │ technique bionty.ExperimentalFactor ✗ ✓ ✓ unset └── rna:var: mudata_papalexi21_subset_rna_var_schema ├── uid: O6tr1w6Xj69kL18B run: 3iq1t42 (multimodal.ipynb) │ itype: bionty.Gene.symbol created_at: 2026-08-25 14:24:31 UTC │ created_by: testuser1 └── bionty.Gene.symbol └── dtype: float
Validate MuData annotations¶
curator = ln.curators.MuDataCurator(mdata, mudata_schema)
Show code cell output
! auto-transposed `var` for backward compat, please indicate transposition in the schema definition by calling out `.T`: slots={'var.T': itype=bt.Gene.ensembl_gene_id}
try:
curator.validate()
except ln.errors.ValidationError:
pass
curator.slots["rna:var"].cat.standardize("columns")
curator.slots["rna:var"].cat.add_new_from("columns")
curator.validate()
Register curated Artifact¶
artifact = curator.save_artifact(key="mudata_papalexi21_subset.h5mu")
Show code cell output
→ returning schema with same hash: Schema(uid='VfdqJgALmYij3GyJ', is_type=False, name='mudata_papalexi21_subset_obs_schema', description=None, n_members=2, coerce=None, flexible=False, itype='Feature', otype=None, suffix=None, hash='efqnwpkNcu4DhwpIUaggqA', minimal_set=True, ordered_set=False, maximal_set=False, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, run_id=1, type_id=None, created_at=2026-08-25 14:24:31 UTC, is_locked=False)
→ returning schema with same hash: Schema(uid='JXUP2jIda3cRslcl', is_type=False, name='mudata_papalexi21_subset_rna_obs_schema', description=None, n_members=3, coerce=True, flexible=False, itype='Feature', otype=None, suffix=None, hash='ywWnrCrgJbpClASrkt7vyQ', minimal_set=True, ordered_set=False, maximal_set=False, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, run_id=1, type_id=None, created_at=2026-08-25 14:24:31 UTC, is_locked=False)
→ returning schema with same hash: Schema(uid='8c6hV7pDUAAhXfXf', is_type=False, name='mudata_papalexi21_subset_hto_obs_schema', description=None, n_members=3, coerce=True, flexible=False, itype='Feature', otype=None, suffix=None, hash='plFdMc46R4QwdoMj_XbNkQ', minimal_set=True, ordered_set=False, maximal_set=False, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, run_id=1, type_id=None, created_at=2026-08-25 14:24:31 UTC, is_locked=False)
artifact.describe()
Show code cell output
Artifact: mudata_papalexi21_subset.h5mu (0000) ├── uid: gg6PbYz1LHwWtevG0000 run: 3iq1t42 (multimodal.ipynb) │ kind: dataset otype: MuData │ hash: yOTamE22ANM-Ph1iqxB7Gw size: 537.1 KB │ branch: main space: all │ created_at: 2026-08-25 14:24:34 UTC created_by: testuser1 │ n_observations: 200 schema: mudata_papalexi21_subset_mudata_schema ├── storage/path: │ /home/runner/work/lamin-usecases/lamin-usecases/docs/test-multimodal/.lamindb/gg6PbYz1LHwWtevG0000.h5mu ├── Dataset features │ ├── obs (2) │ │ perturbation ULabel[Perturbation] NT, Perturbed │ │ replicate ULabel[Replicate] rep1, rep2, rep3 │ ├── rna:obs (3) │ │ nCount_RNA int │ │ nFeature_RNA int │ │ percent.mito float │ ├── hto:obs (3) │ │ nCount_HTO int │ │ nFeature_HTO int │ │ technique bionty.ExperimentalFactor cell hashing │ └── rna:var (184 bionty.Gene.sym… │ ARHGAP26-AS1 num │ CA8 num │ CTAGE15 num │ CTAGE15 num │ GABRA1 num │ H4C12 num │ HLA-DQB1-AS1 num │ HLA-DQB1-AS1 num │ HLA-DQB1-AS1 num │ HLA-DQB1-AS1 num │ HLA-DQB1-AS1 num │ HLA-DQB1-AS1 num │ HLA-DQB1-AS1 num │ MEF2C-AS2 num │ PFKFB1 num │ RBPMS-AS1 num │ SH2D6 num │ SPACA1 num │ TRPC5 num │ VNN1 num └── Labels └── .ulabels ULabel Perturbed, NT, rep1, rep2, rep3 .experimental_factors bionty.ExperimentalFactor cell hashing
ln.finish()
Show code cell output
→ finished Run('3iq1t42PFkqQCHac') after 6s at 2026-08-25 14:24:35 UTC