The AnnData Proteomics Bridge (APB) converts the reports of quantification
software such as DIA-NN, Spectronaut or MaxQuant into one result layout. The
columns keep the vendors’ names, so a posterior error probability is PEP in
MaxQuant and EG_PEP in Spectronaut. apb-catalog records what each column
means; apbCatalogR implements its API in R, so an R application asks for a
meaning and gets the column or its data.
The package works on a bundle apb-catalog releases: the reviewed sets and the levels apb2 converts for each software version. One bundle ships with the package, together with a test bundle of synthetic results and the answers apb-catalog gives on them.
apbCatalogR gives R one way into the quantification outputs of the 14 tools apb2 converts: AlphaDIA, AlphaPept, DIA-NN, FragPipe, i2MassChroQ, MaxQuant, MetaMorpheus, MSAngel, PEAKS, ProlineStudio, quantms, Sage, Spectronaut and WOMBAT-P, and of ProteoBench’s custom upload format. A meaning, such as a PEP or a normalized protein intensity, is asked for by the same key whichever tool reported it. Code that hands a result’s quantities to a Bioconductor package for differential expression analysis is therefore written once for all 14 tools; such packages include DEP, DEqMS, limma, limpa, msqrob2, MSstats, proDA and prolfqua.
apbCatalogR is on Bioconductor because anndataR and MuData are: they bring to
R the AnnData and MuData formats apb2 writes its results in, and apbCatalogR
hands each level back as an anndataR AnnData.
Figure 1: Who does what
The integrator converts the results of quantification software such as DIA-NN, Spectronaut, AlphaDIA and Sage with apb2 and its parsing rules into an APB result, and fills the catalog. Bioconductor applications such as prolfqua, exploreDE and QFeatures read the result by meaning through the apb-catalog API: apbCatalogR in R, apb-catalog in Python, apb-catalog-ts in TypeScript.
The main calls:
Catalog$new(software, software_version, set_name): a version’s fieldsread_parsed_levels(path): an APB result, one anndataR AnnData per levelDataCatalog$new(result, ...): the data of a result, looked up by keysets(), catalog_table(): the sets and their entriesrule_coverage(): how each software version covers a setcatalog_version(), update_catalog(): the bundle in use, and installing oneinstall.packages(c("BiocManager", "remotes"))
BiocManager::install("anndata-omics-bridge/apbCatalogR")
library(apbCatalogR)
catalog_version()
#> $version
#> [1] "0.1.4"
#>
#> $origin
#> [1] "shipped"
#>
#> $path
#> [1] "/tmp/RtmpzDKiPq/Rinst3268873020777e/apbCatalogR/extdata/bundle"
#>
#> $apb2_version
#> [1] "0.1.1"
Each set serves one kind of consumer, and each holds the fields one producer
writes: apb_alpha what apb2 convert writes,
fasta_peptides_alpha what apb-fasta verify-peptides adds,
fasta_proteins_alpha what apb-fasta merge-annotations adds, and
obs_annotation_alpha the sample annotation apb2 annotate attaches:
sets()[, c("set", "has_requirements", "entries")]
#> set has_requirements entries
#> 1 abundance_alpha TRUE 45
#> 2 confidence_alpha FALSE 26
#> 3 proteobench_entrapment_alpha FALSE 7
#> 4 apb_alpha TRUE 181
#> 5 obs_annotation_alpha FALSE 7
#> 6 fasta_peptides_alpha FALSE 28
#> 7 fasta_proteins_alpha FALSE 4
#> 8 chromatography_alpha FALSE 58
catalog_table() lists the entries of a set, one row per field and software
version pattern:
entries <- catalog_table("confidence_alpha")
columns <- c("software_version_pattern", "level", "name", "key")
entries[entries$source == "Spectronaut", columns]
#> software_version_pattern level name key
#> 29 ^(19|20)\\..* peptidoform_ion EG_PEP pep
#> 30 ^15\\..* peptidoform_ion EG_PEP pep
#> 31 ^21\\..* peptidoform_ion EG_PEP pep
#> 32 ^(19|20)\\..* peptidoform_ion EG_Qvalue q_value
#> 33 ^15\\..* peptidoform_ion EG_Qvalue q_value
#> 34 ^21\\..* peptidoform_ion EG_Qvalue q_value
#> 35 ^(19|20)\\..* protein PG_QValue_Run_Wise q_value
#> 36 ^21\\..* protein PG_QValue_Run_Wise q_value
#> 37 ^(19|20)\\..* protein PG_Qvalue q_value
#> 38 ^21\\..* protein PG_Qvalue q_value
#> 39 ^21\\..* protein PG_LibraryQvalue library_q_value
#> 40 ^21\\..* peptidoform_ion EG_LibraryQvalue library_q_value
Catalog answers from the catalog alone, without data. Its levels are the
levels apb2 converts for that software version, each with the slots layers,
var and obs, holding field names:
catalog <- Catalog$new("DIA-NN", "2.2.0", "abundance_alpha")
catalog
#> <Catalog abundance_alpha: peptidoform_ion, protein>
catalog$levels$peptidoform_ion$layers$keys()
#> [1] "intensity" "intensity_ms1"
#> [3] "intensity_ms1_norm_settings" "intensity_norm_settings"
catalog$levels$peptidoform_ion$layers[["intensity"]]
#> [1] "Precursor_Quantity"
catalog$levels$protein$layers[["intensity_norm_settings"]]
#> [1] "PG_MaxLFQ"
[ on var or obs names every column the keys read. Spectronaut 21 reports
two protein q-values:
spectronaut <- Catalog$new("Spectronaut", "21.0", "confidence_alpha")
spectronaut$levels$protein$var$keys()
#> [1] "library_q_value" "q_value"
spectronaut$levels$protein$var[c("q_value", "library_q_value")]
#> [1] "PG_Qvalue" "PG_LibraryQvalue"
$fields() lists every cataloged field of the version as a table; each
entry’s id names the software, version, level and slot where the field lives:
spectronaut$fields()[, c("key", "entry_id")]
#> key entry_id
#> 1 pep Spectronaut/*/peptidoform_ion/layers/EG_PEP/-
#> 2 q_value Spectronaut/*/peptidoform_ion/layers/EG_Qvalue/-
#> 3 library_q_value Spectronaut/21/peptidoform_ion/var/-/EG_LibraryQvalue
#> 4 q_value Spectronaut/21/protein/layers/PG_QValue_Run_Wise/-
#> 5 q_value Spectronaut/21/protein/var/-/PG_Qvalue
#> 6 library_q_value Spectronaut/21/protein/var/-/PG_LibraryQvalue
read_parsed_levels() reads a result apb2 wrote, .parquet, .h5ad or
.duckdb, into ParsedLevels, one anndataR AnnData per level. The test
bundle carries synthetic Parquet results; this one has MaxQuant’s
peptidoform-ion level.
results <- tempfile()
archive <- system.file(
"extdata", "test-bundle", "results.tar.gz",
package = "apbCatalogR"
)
utils::untar(archive, exdir = results)
result <- read_parsed_levels(
file.path(results, "maxquant_wide-peptidoform_ion.parquet")
)
result
#> <ParsedLevels>
#> peptidoform_ion: 2 x 3, layers: Intensity, MS_MS_count, Retention_time, Retention_length, Calibrated_retention_time, Calibrated_retention_time_start, Calibrated_retention_time_finish, Score, PEP
result$levels$peptidoform_ion
#> InMemoryAnnData object with n_obs × n_vars = 2 × 3
#> obs: 'Raw_file', 'Experiment', 'Fraction'
#> var: 'ProForma_ion', 'Sequence', 'Modified_sequence', 'Modifications', 'Charge', 'Mass', 'Proteins', 'Leading_proteins', 'Leading_razor_protein', 'protein_Protein_IDs', 'Protein_IDs', 'ProForma_peptidoform', 'ProForma_peptide', 'Reverse', 'Potential_contaminant', 'apb_Decoy', 'apb_Contaminant'
#> uns: 'apb'
#> layers: 'MS_MS_count', 'Retention_time', 'Retention_length', 'Calibrated_retention_time', 'Calibrated_retention_time_start', 'Calibrated_retention_time_finish', 'Score', 'PEP'
DataCatalog answers on the result and hands back the data. It composes the
named sets alone: apb_alpha, APB’s own fields, answers only when named.
confidence <- DataCatalog$new(result, "confidence_alpha", "apb_alpha")
pep <- confidence$levels$peptidoform_ion$layers[["pep"]]
dim(pep)
#> [1] 2 3
$fields() lists the cataloged fields the result holds, from both sets:
confidence$fields()[, c("set", "slot", "name", "key", "present")]
#> set slot name key present
#> 1 confidence_alpha layers PEP pep TRUE
#> 2 apb_alpha var ProForma_peptide peptide TRUE
#> 3 apb_alpha var ProForma_peptidoform peptidoform TRUE
#> 4 apb_alpha var ProForma_ion peptidoform_ion TRUE
#> 5 apb_alpha var apb_Decoy decoy TRUE
#> 6 apb_alpha var apb_Contaminant contaminant TRUE
#> 7 apb_alpha var Protein_IDs protein_assignment TRUE
#> 8 apb_alpha var Proteins fasta_accessions TRUE
Each level of the catalog has anndataR’s slots. A name is read as a key first,
then as the level’s own layer or column, as the Python package
cf_xarray finds an xarray variable by its
CF-convention standard name first. ?Level and ?Slot list the methods:
ion <- confidence$levels$peptidoform_ion
dim(ion$layers[["pep"]]) # a matrix, runs x features
#> [1] 2 3
ion$layers$keys() # a character vector
#> [1] "pep"
head(ion$var[["peptidoform_ion"]]) # a vector, one value per feature
#> [1] "F0" "F1" "F2"
The slots read the anndataR AnnData of the level, so a key gives the same
data as the vendor’s name in anndataR. A vendor’s own name still reads, and [
gives a data frame, as on anndataR’s obs and var:
# The anndataR AnnData of the level
adata <- result$levels$peptidoform_ion
identical(ion$layers[["pep"]], adata$layers[["PEP"]])
#> [1] TRUE
identical(ion$X, adata$X)
#> [1] TRUE
ion$obs_names
#> [1] "S0" "S1"
ion$var_names
#> [1] "F0" "F1" "F2"
# MaxQuant's own layer, as adata$layers[["Score"]]
ion$layers[["Score"]]
#> F0 F1 F2
#> S0 0.8 0.9 1.0
#> S1 0.9 1.0 1.1
# A data frame, as adata$obs[c("Raw_file", "Experiment")]
ion$obs[c("Raw_file", "Experiment")]
#> Raw_file Experiment
#> 1 S0 v0
#> 2 S1 v1
# Keys, each column named as the result names it
ion$var[c("peptidoform_ion", "decoy", "contaminant")]
#> ProForma_ion apb_Decoy apb_Contaminant
#> 1 F0 TRUE TRUE
#> 2 F1 FALSE FALSE
#> 3 F2 TRUE TRUE
A vendor may name a column as a key is named, as Sage names its quantification
q-value q_value. A prefix makes the lookup exact: a set’s prefix asks that set
alone and never the level’s own field, v: the level’s own field alone and
never the catalog. sets() lists each set’s prefix:
ion$layers$name("c:pep") # the confidence set alone
#> [1] "PEP"
ion$layers$name("v:Score") # MaxQuant's own layer alone
#> [1] "Score"
sets()[, c("set", "prefix")]
#> set prefix
#> 1 abundance_alpha a
#> 2 confidence_alpha c
#> 3 proteobench_entrapment_alpha pbe
#> 4 apb_alpha apb
#> 5 obs_annotation_alpha annot
#> 6 fasta_peptides_alpha fpep
#> 7 fasta_proteins_alpha fprot
#> 8 chromatography_alpha rt
MaxQuant reports no q-value on this level, so $get() gives NULL, where
[[ signals an error; $name() names the field a key reads:
ion$layers$get("q_value")
#> NULL
ion$layers$name("pep")
#> [1] "PEP"
A lookup the catalog cannot answer with one field signals UnresolvedField
rather than choosing silently. Spectronaut does not document how its protein
quantity combines the precursors, so asking for a summed one is unknown there:
spectronaut <- Catalog$new("Spectronaut", "21.0", "abundance_alpha")
tryCatch(
spectronaut$levels$protein$layers$get(
"intensity_norm_settings",
aggregation = "sum"
),
UnresolvedField = function(condition) conditionMessage(condition)
)
#> [1] "intensity_norm_settings (kind=intensity, normalization=run_setting, aggregation=sum) on protein is unknown: PG_Quantity leaves aggregation unknown; candidates: PG_Quantity"
The same lookup reads each software’s field; the test bundle holds results of many:
files <- c(
MaxQuant = "maxquant_wide-peptidoform_ion.parquet",
Spectronaut = "spectronaut_v21-peptidoform_ion.parquet",
`DIA-NN` = "diann_v2-peptidoform_ion.parquet"
)
pep_field <- function(file) {
vendor <- read_parsed_levels(file.path(results, file))
catalog <- DataCatalog$new(vendor, "confidence_alpha")
catalog$levels$peptidoform_ion$layers$name("pep")
}
vapply(files, pep_field, character(1))
#> MaxQuant Spectronaut DIA-NN
#> "PEP" "EG_PEP" "PEP"
A protein level reads the same way; DIA-NN’s MaxLFQ intensity, normalized as
the run’s settings chose, is the key intensity_norm_settings:
diann <- read_parsed_levels(file.path(results, "diann_v2-protein.parquet"))
protein <- DataCatalog$new(diann, "abundance_alpha")$levels$protein
protein$layers$keys()
#> [1] "intensity_norm_settings"
protein$layers$name("intensity_norm_settings")
#> [1] "PG_MaxLFQ"
protein$layers[["intensity_norm_settings"]]
#> F0 F1 F2
#> S0 0.0 0.1 0.2
#> S1 0.1 0.2 0.3
DataCatalog$new() composes several sets on one result. A key may leave a
qualifier open, such as the unit of a retention time; $get() and $name()
narrow it, and a lookup no field answers gives NULL:
both <- DataCatalog$new(result, "confidence_alpha", "chromatography_alpha")
both
#> <DataCatalog confidence_alpha, chromatography_alpha: peptidoform_ion>
rt <- both$levels$peptidoform_ion
rt$layers$keys()
#> [1] "aligned_peak_end" "aligned_peak_start" "aligned_retention_time"
#> [4] "peak_width" "pep" "retention_time"
rt$layers$name("retention_time", unit = "minute")
#> [1] "Retention_time"
rt$layers$get("retention_time", unit = "second")
#> NULL
A set of an app’s fields answers once the app has run, and the sample
annotation once apb2 annotate has attached it. Opening such a set on a result
that lacks them is an error that says what to run:
tryCatch(
DataCatalog$new(result, "fasta_peptides_alpha"),
error = function(condition) conditionMessage(condition)
)
#> [1] "fasta_peptides_alpha: apb-fasta has not run on this result; run `apb-fasta verify-peptides`"
DataCatalog$answering_sets() lists the sets a result opens, without an error,
so a consumer can leave out the fields of an app that has not run:
DataCatalog$answering_sets(result)
#> [1] "abundance_alpha" "confidence_alpha"
#> [3] "proteobench_entrapment_alpha" "apb_alpha"
#> [5] "chromatography_alpha"
A set may require or recommend fields. enforce checks them once, when the
catalog opens: "required" signals one MissingRequiredField listing every
missing required field and warns about missing recommended ones;
"recommended" signals for both. MaxQuant’s peptidoform level lacks a
recommended field of apb_alpha:
peptidoform <- read_parsed_levels(
file.path(results, "maxquant_wide-peptidoform.parquet")
)
tryCatch(
DataCatalog$new(peptidoform, "apb_alpha", enforce = "recommended"),
MissingRequiredField = function(condition) conditionMessage(condition)
)
#> [1] "recommended field peptidoform (kind=peptidoform) on peptidoform is missing: no reviewed MaxQuant ^(1\\.5|1\\.6|2\\.) entry on peptidoform matches"
rule_coverage() checks every rule against one set; a set with requirements
judges each level:
coverage <- rule_coverage("abundance_alpha")
unique(coverage[, c("software", "version", "level", "verdict")])[1:5, ]
#> software version level verdict
#> 1 AlphaDIA 1.10 peptidoform_ion warn
#> 3 AlphaDIA 1.12 peptidoform_ion warn
#> 5 AlphaDIA 2.x peptidoform_ion warn
#> 7 AlphaDIA 2.x protein <NA>
#> 8 AlphaPept 0.5 peptidoform_ion pass
update_catalog() installs a bundle archive into the user cache; later
sessions use the newest bundle, shipped or installed. apb-catalog’s
make bundle writes the archive. Nothing is downloaded.
update_catalog("apb-catalog/build/apb-catalog-bundle.tar.gz")
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.5 LTS
#>
#> Matrix products: default
#> BLAS: /home/biocbuild/bbs-3.24-bioc/R/lib/libRblas.so
#> LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0 LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_GB LC_COLLATE=C
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: America/New_York
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] apbCatalogR_0.99.4 BiocStyle_2.41.0
#>
#> loaded via a namespace (and not attached):
#> [1] vctrs_0.7.3 cli_3.6.6 knitr_1.52
#> [4] rlang_1.3.0 xfun_0.62 otel_0.3.0
#> [7] png_0.1-9 purrr_1.2.2 assertthat_0.2.1
#> [10] jsonlite_2.0.0 glue_1.8.1 bit_4.6.0
#> [13] htmltools_0.5.9 sass_0.4.10 rmarkdown_2.32
#> [16] grid_4.6.1 evaluate_1.0.5 jquerylib_0.1.4
#> [19] fastmap_1.2.0 yaml_2.3.12 lifecycle_1.0.5
#> [22] bookdown_0.48 BiocManager_1.30.27 compiler_4.6.1
#> [25] Rcpp_1.1.2 anndataR_1.3.5 lattice_0.23-1
#> [28] digest_0.6.39 R6_2.6.1 tidyselect_1.2.1
#> [31] reticulate_1.47.0 magrittr_2.0.5 bslib_0.12.0
#> [34] Matrix_1.7-6 bit64_4.8.6 tools_4.6.1
#> [37] arrow_26.0.1 cachem_1.1.0