Skip to main content

Looking up APB fields by meaning

Author

Witold Wolski

Introduction

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 catalogue 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.

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, version, set): the fields a version reports
  • read_parsed_levels(path): an APB result, one anndataR AnnData per level
  • DataCatalog$new(result, ...): the data of a result, looked up by key
  • catalogues(), catalogue_table(): the sets and their entries
  • rule_coverage(): how each software version covers a set
  • catalog_version(), update_catalog(): the bundle in use, and a newer one

Installation

install.packages(c("BiocManager", "remotes"))
BiocManager::install("anndata-omics-bridge/apbCatalogR")
library(apbCatalogR)
catalog_version()
#> $version
#> [1] "0.1.4"
#> 
#> $origin
#> [1] "shipped"
#> 
#> $path
#> [1] "/home/runner/work/_temp/Library/apbCatalogR/extdata/bundle"
#> 
#> $apb2_version
#> [1] "0.1.1"

Catalogue sets

Each catalogue set serves one kind of consumer:

catalogues()[, c("catalogue", "has_requirements", "entries")]
#>                catalogue has_requirements entries
#> 1              abundance             TRUE      45
#> 2             confidence            FALSE      26
#> 3 proteobench_entrapment            FALSE       7
#> 4                    apb             TRUE     268
#> 5         chromatography            FALSE      58

catalogue_table() lists the entries of a set, one row per field and software version pattern:

entries <- catalogue_table("confidence")
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

Which column a software version reports

Catalog answers from the catalogue 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")
catalog
#> <Catalog abundance: peptidoform_ion, protein>
catalog$levels$peptidoform_ion$layers$keys()
#> [1] "intensity"                "ms1_intensity"           
#> [3] "normalized_intensity"     "normalized_ms1_intensity"
catalog$levels$peptidoform_ion$layers[["intensity"]]
#> [1] "Precursor_Quantity"
catalog$levels$protein$layers[["maxlfq"]]
#> [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")
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 catalogued 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

The data of a result

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', 'Leading_Proteins_Legacy', 'Leading_Razor_Protein_Legacy', 'Razor_Protein_Group', 'Protein_IDs', 'ProForma_peptidoform', 'ProForma_peptide', 'Reverse', 'Decoy', '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:

confidence <- DataCatalog$new(result, "confidence")
pep <- confidence$levels$peptidoform_ion$layers[["pep"]]
dim(pep)
#> [1] 2 3

$fields() lists the catalogued fields the result holds, from the confidence set and from apb, which is always in:

confidence$fields()[, c("set", "location", "name", "key", "present")]
#>          set location                 name                key present
#> 1 confidence   layers                  PEP                pep    TRUE
#> 2        apb      var     ProForma_peptide            peptide    TRUE
#> 3        apb      var ProForma_peptidoform        peptidoform    TRUE
#> 4        apb      var         ProForma_ion    peptidoform_ion    TRUE
#> 5        apb      var            apb_Decoy              decoy    TRUE
#> 6        apb      var      apb_Contaminant        contaminant    TRUE
#> 7        apb      var          Protein_IDs protein_assignment    TRUE
#> 8        apb      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 cf_xarray reads ds.cf[...]; apb, APB’s own fields, is always in:

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.7 0.8 0.9
#> S1 0.8 0.9 1.0
# 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

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 catalogue cannot answer with one field signals UnresolvedField rather than choosing silently. FragPipe does not document whether its intensity is normalized, so the key intensity, never normalized, is unknown there:

fragpipe <- Catalog$new("FragPipe", "22.0", "abundance")
tryCatch(
    fragpipe$levels$peptidoform_ion$layers[["intensity"]],
    UnresolvedField = function(condition) conditionMessage(condition)
)
#> [1] "intensity (kind=intensity, normalization=none) on peptidoform_ion is unknown: Intensity leaves normalization unknown; candidates: Intensity"

The same code for every software

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")
    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 is the key maxlfq:

diann <- read_parsed_levels(file.path(results, "diann_v2-protein.parquet"))
protein <- DataCatalog$new(diann, "abundance")$levels$protein
protein$layers$keys()
#> [1] "maxlfq"
protein$layers$name("maxlfq")
#> [1] "PG_MaxLFQ"
protein$layers[["maxlfq"]]
#>     F0  F1  F2
#> S0 0.0 0.1 0.2
#> S1 0.1 0.2 0.3

Several sets and qualifiers

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", "chromatography")
both
#> <DataCatalog confidence, chromatography, apb: 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

Rule coverage

rule_coverage() checks every rule against one set; a set with requirements judges each level:

coverage <- rule_coverage("abundance")
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

A newer catalog

update_catalog() downloads the bundle of the latest apb-catalog release into the user cache; later sessions use the newest bundle, shipped or downloaded. It works from the next apb-catalog release, the first to ship the bundle.

Session information

sessionInfo()
#> R version 4.6.2 (2026-10-29)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.5 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
#>  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
#>  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
#> [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
#> 
#> time zone: UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] apbCatalogR_0.99.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] vctrs_0.7.3       cli_3.6.6         knitr_1.52        rlang_1.3.0      
#>  [5] xfun_0.62         otel_0.2.0        png_0.1-9         purrr_1.2.2      
#>  [9] assertthat_0.2.1  jsonlite_2.0.0    glue_1.8.1        bit_4.6.0        
#> [13] htmltools_0.5.9   rmarkdown_2.32    grid_4.6.2        evaluate_1.0.5   
#> [17] fastmap_1.2.0     yaml_2.3.12       lifecycle_1.0.5   compiler_4.6.2   
#> [21] Rcpp_1.1.2        htmlwidgets_1.6.4 anndataR_1.2.3    lattice_0.23-1   
#> [25] digest_0.6.39     R6_2.6.1          reticulate_1.47.0 tidyselect_1.2.1 
#> [29] magrittr_2.0.5    Matrix_1.7-6      tools_4.6.2       bit64_4.8.6      
#> [33] arrow_26.0.0