SpaMTP’s current annotation pipeline separates candidate generation from downstream filtering. It generates chemically valid adduct hypotheses once, sorts their expected mass-to-charge ratios, queries that index within a strict mass tolerance, and ranks the remaining candidates using optional MS1 context.
The workflow is:
This article describes upstream candidate annotation using native Bioconductor containers. See the native workflow for downstream spatial plotting and pathway-based candidate ranking, and modern data access for container accessors.
SpaMTP uses a deliberately pruned, versioned RaMP snapshot.
SpaMTPdb stores the annotation and pathway resources
independently of the analysis code: the chemical table keeps structures,
formulae, monoisotopic masses, names, source identifiers, and current
RAMP_C_* identifiers, while pathway tables retain only
fields required by SpaMTP’s enrichment and network functions. The
source = "auto" setting resolves these versioned resources
through SpaMTPdb; a local staged resource directory can be
used offline. The registry itself is package metadata, so it can be
inspected without downloading the large chemical table.
ramp_registry <- spaMTPDatabaseInfo()
chem_props_registry <- ramp_registry[
ramp_registry$resource == "chem_props",
,
drop = FALSE
]ramp_summary <- data.frame(
property = c(
"RaMP version",
"Chemical-property records",
"Serialized resource size",
"Resource checksum"
),
value = c(
chem_props_registry$version,
format(chem_props_registry$rows, big.mark = ","),
format(chem_props_registry$bytes, big.mark = ","),
chem_props_registry$md5
)
)
knitr::kable(ramp_summary, align = c("l", "r"))| property | value |
|---|---|
| RaMP version | 3.0.7 |
| Chemical-property records | 289,754 |
| Serialized resource size | 12,925,616 |
| Resource checksum | 42865f00ea7445f72bcd2f66b2b315a9 |
The complete table is retrieved only when it is needed. Its versioned
schema is listed in the companion package registry and documented by
SpaMTPdb:
chem_props_registry[, c(
"resource", "version", "rows", "columns", "rdata_path"
)] |>
knitr::kable(align = c("l", "r", "r", "r", "l"))| resource | version | rows | columns | rdata_path |
|---|---|---|---|---|
| chem_props | 3.0.7 | 289754 | 11 | chem_props.rds/content |
The examples below use the RaMP entry for D-2-hydroxyglutaric acid
(2HG), with neutral monoisotopic mass approximately 148.0372 Da. Rather
than manually declaring its chemistry, SpaMTP decomposes the RaMP
iso_smiles graph into recognisable functional groups and
ionisation sites.
# This one-row subset is embedded in the article so every example is runnable
# without downloading the 289,754-row resource. The identifiers and structure
# are copied verbatim from SpaMTPdb 3.0.7.
target_db <- data.frame(
ramp_id = "RAMP_C_000219574",
chem_data_source = "hmdb",
chem_source_id = "hmdb:HMDB0000606",
inchi_key = "HWXBTNAVRSUOJR-GSVOUGTGSA-N",
monoisotop_mass = 148.037173366,
common_name = "D-2-Hydroxyglutaric acid",
mol_formula = "C5H8O5",
iso_smiles = "O[C@H](CCC(O)=O)C(O)=O",
stringsAsFactors = FALSE
)
target_db <- annotateSMILESStructure(target_db)
knitr::kable(
target_db[, c(
"ramp_id", "chem_source_id", "common_name", "mol_formula",
"monoisotop_mass", "iso_smiles", "carboxyl_sites",
"alcohol_hydroxyl_sites", "ketone_sites",
"max_exchangeable_protons"
)],
digits = 7
)| ramp_id | chem_source_id | common_name | mol_formula | monoisotop_mass | iso_smiles | carboxyl_sites | alcohol_hydroxyl_sites | ketone_sites | max_exchangeable_protons |
|---|---|---|---|---|---|---|---|---|---|
| RAMP_C_000219574 | hmdb:HMDB0000606 | D-2-Hydroxyglutaric acid | C5H8O5 | 148.0372 | OC@HC(O)=O | 2 | 1 | 0 | 2 |
The native parser works on molecular connectivity rather than elemental formula: for example, it separates a carboxylic acid from an ester and a ketone from the carbonyl in a carboxyl group. Existing curated columns are preserved by default; missing values are filled from SMILES.
deconvolveSMILES() also derives mode-specific, bounded
scores. They are chemical priors, not calibrated probabilities or
substitutes for experimental evidence.
structure_summary <- target_db[, c(
"carboxyl_sites", "hydroxyl_sites", "carbonyl_sites",
"acidic_sites", "basic_sites", "proton_acceptor_sites",
"alkali_binding_sites", "alkali_chelation_motifs",
"positive_mode_score", "negative_mode_score",
"alkali_affinity_score", "neutral_mass_score"
)]
knitr::kable(structure_summary, digits = 2)| carboxyl_sites | hydroxyl_sites | carbonyl_sites | acidic_sites | basic_sites | proton_acceptor_sites | alkali_binding_sites | alkali_chelation_motifs | positive_mode_score | negative_mode_score | alkali_affinity_score | neutral_mass_score |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2 | 3 | 2 | 2 | 0 | 2 | 5 | 2 | 0.52 | 0.94 | 0.99 | 1 |
SpaMTP interprets these fields differently for each search mode:
[M+Na]+ or [M+K]+ even when
it is not strongly basic.neutral is a neutral-mass matching mode, not a claim
that a mass spectrometer directly detected an uncharged molecule. It
applies no protonation or metal-binding requirement.For 2HG, two carboxyl groups support negative-mode deprotonation,
while its five oxygen donors and multiple local donor motifs give
stronger support to an alkali adduct than to simple positive-mode
protonation. The complete audit text is retained in
structure_evidence:
target_db[, c(
"positive_site_atoms", "negative_site_atoms", "alkali_site_atoms",
"structure_evidence"
)]## positive_site_atoms negative_site_atoms alkali_site_atoms
## 1 O7,O10 O6,O9 O1,O6,O7,O9,O10
## structure_evidence
## 1 carboxyl=2; alcohol-OH=1; phenolic-OH=0; ketone=0; amine=0; acidic-sites=2; proton-acceptors=2; alkali-donors=5; chelation-motifs=2; positive-atoms=O7,O10; negative-atoms=O6,O9; alkali-atoms=O1,O6,O7,O9,O10
Atom labels such as O1 refer to the atom order in the
supplied SMILES graph; they identify the proposed local sites and are
not canonical atom-map numbers.
The structure-only adduct search space can be inspected before any
peak is queried. No adducts argument is required:
predicted_spaces <- lapply(
c("positive", "negative", "neutral"),
function(mode) {
x <- predictAdductsFromSMILES(target_db$iso_smiles, polarity = mode)
x <- x[x$retained, ]
utils::head(x, 4L)
}
)
predicted_spaces <- do.call(rbind, predicted_spaces)
knitr::kable(
predicted_spaces[, c(
"polarity", "adduct", "structure_score", "proton_bound_status",
"combined_prior", "retained"
)],
digits = 3,
row.names = FALSE
)| polarity | adduct | structure_score | proton_bound_status | combined_prior | retained |
|---|---|---|---|---|---|
| positive | M+Na | 0.99 | not_required | 0.891 | TRUE |
| positive | M+K | 0.99 | not_required | 0.792 | TRUE |
| positive | M+H+Na | 0.99 | not_required | 0.693 | TRUE |
| positive | M+2Na | 0.99 | not_required | 0.643 | TRUE |
| negative | M-H | 0.94 | preferred-sites | 0.940 | TRUE |
| negative | M-2H | 0.94 | preferred-sites | 0.752 | TRUE |
| negative | M+Na-2H | 0.99 | preferred-sites | 0.594 | TRUE |
| negative | M+Cl | 0.90 | not_required | 0.585 | TRUE |
| neutral | M | 1.00 | not_required | 1.000 | TRUE |
This prediction selects plausible families; observed mass, isotope, co-occurring adducts, spatial evidence, and pseudo-MS/MS still decide whether a specific peak supports one of them.
adductRules() returns the ion masses, molecular
multiplier, charge, proton-loss count, chemical class, and prior used by
the engine. Bracketed and unbracketed forms such as [M+H]+
and M+H are both accepted.
selected_adducts <- c("M+H", "M+Na", "M+2Na-H", "2M+H")
positive_rules <- adductRules("positive")
rule_summary <- positive_rules[
positive_rules$name %in% selected_adducts,
c(
"name", "n_molecules", "mass_shift", "charge", "loss_h",
"complexity", "base_adduct", "prior"
)
]
rule_summary <- rule_summary[match(selected_adducts, rule_summary$name), ]
knitr::kable(rule_summary, digits = 7, row.names = FALSE)| name | n_molecules | mass_shift | charge | loss_h | complexity | base_adduct | prior |
|---|---|---|---|---|---|---|---|
| M+H | 1 | 1.007276 | 1 | 0 | simple | M+H | 1.00 |
| M+Na | 1 | 22.989221 | 1 | 0 | salt | M+Na | 0.90 |
| M+2Na-H | 1 | 44.971165 | 1 | 1 | metal_exchange | M+Na | 0.65 |
| 2M+H | 2 | 1.007276 | 1 | 0 | multimer | M+H | 0.55 |
For each rule, SpaMTP calculates
\[ m/z_{expected} = \frac{nM + \Delta m_{adduct} - \Delta m_{loss}}{|z|}. \]
The stored mass_shift already contains the signed sum of
additions and losses. Custom rule tables must satisfy charge balance
before candidate generation.
buildMZAnnotationIndex() performs the expensive
expansion once. Before mass expansion it applies SMILES-derived,
rule-specific priors and chemical bounds; expected ions that remain are
sorted for binary interval search. Thus the engine does not treat every
adduct as equally plausible and only rank by m/z afterwards.
positive_index <- buildMZAnnotationIndex(
target_db,
polarity = "positive",
adducts = selected_adducts
)
positive_index## SpaMTP m/z annotation index
## polarity: positive
## MALDI matrix profile: unspecified
## metabolites: 1
## SMILES structures parsed: 1
## ion/reaction rules: 4
## expected ions: 4
ion_table <- data.frame(
adduct = positive_index$rules$name[positive_index$rule_idx],
expected_mz = positive_index$expected_mz
)
knitr::kable(ion_table, digits = 9, row.names = FALSE)| adduct | expected_mz |
|---|---|
| M+H | 149.0444 |
| M+Na | 171.0264 |
| M+2Na-H | 193.0083 |
| 2M+H | 297.0816 |
The structure contribution for each retained hypothesis is directly inspectable:
structure_rank <- queryMZAnnotationIndex(
positive_index$expected_mz,
positive_index,
ppm = 0,
check_isotopes = FALSE,
check_adduct_network = FALSE
)
knitr::kable(
structure_rank[, c(
"adduct", "expected_mz", "structure_score", "structure_status",
"structure_rule_evidence"
)],
digits = 3,
row.names = FALSE
)| adduct | expected_mz | structure_score | structure_status | structure_rule_evidence |
|---|---|---|---|---|
| M+H | 149.044 | 0.52 | structure-supported | positive-mode proton/cation acceptance: carboxyl=2; alcohol-OH=1; phenolic-OH=0; ketone=0; amine=0; acidic-sites=2; proton-acceptors=2; alkali-donors=5; chelation-motifs=2; positive-atoms=O7,O10; negative-atoms=O6,O9; alkali-atoms=O1,O6,O7,O9,O10 |
| M+Na | 171.026 | 0.99 | structure-supported | alkali/metal coordination: carboxyl=2; alcohol-OH=1; phenolic-OH=0; ketone=0; amine=0; acidic-sites=2; proton-acceptors=2; alkali-donors=5; chelation-motifs=2; positive-atoms=O7,O10; negative-atoms=O6,O9; alkali-atoms=O1,O6,O7,O9,O10 |
| M+2Na-H | 193.008 | 0.99 | structure-supported | alkali/metal coordination: carboxyl=2; alcohol-OH=1; phenolic-OH=0; ketone=0; amine=0; acidic-sites=2; proton-acceptors=2; alkali-donors=5; chelation-motifs=2; positive-atoms=O7,O10; negative-atoms=O6,O9; alkali-atoms=O1,O6,O7,O9,O10 |
| 2M+H | 297.082 | 0.52 | structure-supported | positive-mode proton/cation acceptance: carboxyl=2; alcohol-OH=1; phenolic-OH=0; ketone=0; amine=0; acidic-sites=2; proton-acceptors=2; alkali-donors=5; chelation-motifs=2; positive-atoms=O7,O10; negative-atoms=O6,O9; alkali-atoms=O1,O6,O7,O9,O10 |
For large datasets, build one index for each database, polarity, and adduct set, then reuse it for every spectrum or batch of observed peaks.
The 2HG record permits at most two proton losses. Therefore
[M-3H]3- is not included in the indexed search space.
negative_index <- buildMZAnnotationIndex(
target_db,
polarity = "negative",
adducts = c("M-H", "M-2H", "M-3H")
)
indexed_negative_rules <- unique(
negative_index$rules$name[negative_index$rule_idx]
)
data.frame(indexed_rule = indexed_negative_rules) |>
knitr::kable(align = "l")| indexed_rule |
|---|
| M-2H |
| M-H |
The following observed peaks have small controlled mass offsets. The
contextual MS1 spectrum also contains a carbon-13 peak for
[M+H]+ at +1.00335 Da. In real data,
ms1_spectrum should represent the same retention-time
window, spatial pixel, or biologically coherent region as the queried
peaks.
ppm_offsets <- c(1.2, -1.4, 0.6, -0.8)
observed_mz <- ion_table$expected_mz * (1 + ppm_offsets * 1e-6)
names(observed_mz) <- ion_table$adduct
carbon13_spacing <- 1.00335483507
ms1_spectrum <- data.frame(
mz = c(
observed_mz,
ion_table$expected_mz[ion_table$adduct == "M+H"] + carbon13_spacing
),
intensity = c(1000, 700, 300, 200, 53.5)
)
candidates <- queryMZAnnotationIndex(
observed_mz,
positive_index,
ppm = 5,
ms1_spectrum = ms1_spectrum,
min_score = 0
)
candidate_view <- candidates[, c(
"observed_mz", "expected_mz", "adduct", "ppm_error", "mass_score",
"rule_prior", "chemical_score", "structure_score", "structure_status",
"isotope_score",
"adduct_network_score", "score", "ramp_ids"
)]
knitr::kable(candidate_view, digits = 4, row.names = FALSE)| observed_mz | expected_mz | adduct | ppm_error | mass_score | rule_prior | chemical_score | structure_score | structure_status | isotope_score | adduct_network_score | score | ramp_ids |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 149.0446 | 149.0444 | M+H | 1.2 | 0.7717 | 1.00 | 1 | 0.52 | structure-supported | 1.0 | NA | 0.4013 | RAMP_C_000219574 |
| 171.0262 | 171.0264 | M+Na | 1.4 | 0.7027 | 0.90 | 1 | 0.99 | structure-supported | 0.5 | NA | 0.3131 | RAMP_C_000219574 |
| 193.0085 | 193.0083 | M+2Na-H | 0.6 | 0.9373 | 0.65 | 1 | 0.99 | structure-supported | 0.5 | 1 | 0.3016 | RAMP_C_000219574 |
| 297.0814 | 297.0816 | 2M+H | 0.8 | 0.8912 | 0.55 | 1 | 0.52 | structure-supported | 0.5 | 1 | 0.1274 | RAMP_C_000219574 |
Candidates outside the requested tolerance are removed before scoring:
outside_five_ppm <-
ion_table$expected_mz[ion_table$adduct == "M+H"] * (1 + 5.1e-6)
data.frame(
query = "5.1 ppm outside expected m/z",
retained_candidates = nrow(queryMZAnnotationIndex(
outside_five_ppm,
positive_index,
ppm = 5
))
) |>
knitr::kable(row.names = FALSE)| query | retained_candidates |
|---|---|
| 5.1 ppm outside expected m/z | 0 |
The final score is multiplicative:
\[ S = S_{mass} \times P_{rule} \times S_{chemical} \times S_{structure} \times S_{reaction\ site} \times S_{isotope} \times S_{adduct\ family}. \]
Missing optional MS1 context is neutral rather than evidence of absence. When a contextual spectrum is supplied, missing expected isotope or base-adduct peaks can reduce the score.
A complex ion is more credible when a corresponding base monomer is present in the same spectrum. Here isotope scoring is disabled so that only adduct-network support changes.
dimer_mz <- ion_table$expected_mz[ion_table$adduct == "2M+H"]
monomer_mz <- ion_table$expected_mz[ion_table$adduct == "M+H"]
isolated_dimer <- queryMZAnnotationIndex(
dimer_mz,
positive_index,
ppm = 5,
ms1_spectrum = data.frame(mz = dimer_mz, intensity = 100),
check_isotopes = FALSE
)
supported_dimer <- queryMZAnnotationIndex(
dimer_mz,
positive_index,
ppm = 5,
ms1_spectrum = data.frame(
mz = c(dimer_mz, monomer_mz),
intensity = c(100, 500)
),
check_isotopes = FALSE
)
data.frame(
context = c("isolated dimer", "base monomer present"),
adduct_network_score = c(
isolated_dimer$adduct_network_score,
supported_dimer$adduct_network_score
),
final_score = c(isolated_dimer$score, supported_dimer$score)
) |>
knitr::kable(digits = 3, row.names = FALSE)| context | adduct_network_score | final_score |
|---|---|---|
| isolated dimer | 0.1 | 0.029 |
| base monomer present | 1.0 | 0.286 |
For a nominally CHO-only candidate, an unusually negative signed mass defect is treated as background-like evidence. It receives a penalty rather than being deleted, because halogenated, metal-containing, and unusual biological ions must not be rejected by a universal fractional-mass rule.
background_like_db <- data.frame(
formula = "C2H2O2",
exactmass = 99.942723533379,
id = "synthetic-background-like-candidate",
name = "Synthetic background-like candidate"
)
background_index <- buildMZAnnotationIndex(
background_like_db,
polarity = "positive",
adducts = "M+H"
)
background_mz <- background_index$expected_mz[[1L]]
with_penalty <- queryMZAnnotationIndex(
background_mz,
background_index,
ppm = 0,
use_mass_defect = TRUE,
check_isotopes = FALSE,
check_adduct_network = FALSE
)
without_penalty <- queryMZAnnotationIndex(
background_mz,
background_index,
ppm = 0,
use_mass_defect = FALSE,
check_isotopes = FALSE,
check_adduct_network = FALSE
)
data.frame(
setting = c("mass-defect heuristic on", "mass-defect heuristic off"),
signed_mass_defect = background_mz - round(background_mz),
chemical_score = c(
with_penalty$chemical_score,
without_penalty$chemical_score
),
retained = TRUE
) |>
knitr::kable(digits = 3, row.names = FALSE)| setting | signed_mass_defect | chemical_score | retained |
|---|---|---|---|
| mass-defect heuristic on | -0.05 | 0.2 | TRUE |
| mass-defect heuristic off | -0.05 | 1.0 | TRUE |
annotateMZ() is a convenience wrapper for individual or
small batches of observed values. Supply a pre-built index to avoid
rebuilding it.
single_result <- annotateMZ(
observed_mz[["M+H"]],
index = positive_index,
polarity = "positive",
ppm = 5,
ms1_spectrum = ms1_spectrum,
min_score = 0
)
knitr::kable(
single_result[, c(
"observed_mz", "metabolite_names", "ramp_ids", "adduct",
"ppm_error", "score"
)],
digits = 4,
row.names = FALSE
)| observed_mz | metabolite_names | ramp_ids | adduct | ppm_error | score |
|---|---|---|---|---|---|
| 149.0446 | D-2-Hydroxyglutaric acid | RAMP_C_000219574 | M+H | 1.2 | 0.4013 |
annotateSM() applies the same engine to m/z feature
metadata and preserves the ranked candidate table in
metadata()$mz_annotation. The small object below is only
for demonstrating storage and provenance.
demo_counts <- Matrix::Matrix(
matrix(
seq_len(length(observed_mz) * 4L),
nrow = length(observed_mz),
ncol = 4L
),
sparse = TRUE
)
rownames(demo_counts) <- paste0("mz-", observed_mz)
colnames(demo_counts) <- paste0("pixel_", seq_len(ncol(demo_counts)))
demo_object <- SingleCellExperiment::SingleCellExperiment(
assays = list(counts = demo_counts),
rowData = S4Vectors::DataFrame(
raw_mz = unname(observed_mz),
row.names = rownames(demo_counts)
)
)
annotated_object <- annotateSM(
demo_object,
db = NULL,
assay = "main",
raw.mz.column = "raw_mz",
ppm_error = 5,
adducts = selected_adducts,
polarity = "positive",
return.only.annotated = FALSE,
save.intermediate = TRUE,
min_score = 0,
verbose = FALSE,
index = positive_index,
ms1_spectrum = ms1_spectrum
)
annotation_columns <- as.data.frame(SummarizedExperiment::rowData(annotated_object))[, c(
"raw_mz", "all_IsomerNames", "all_Adducts", "all_Scores",
"all_Ramp_IDs"
)]
knitr::kable(annotation_columns, digits = 4)| raw_mz | all_IsomerNames | all_Adducts | all_Scores | all_Ramp_IDs | |
|---|---|---|---|---|---|
| mz-149.044628685961 | 149.0446 | D-2-Hydroxyglutaric acid | M+H | 0.4013 | RAMP_C_000219574 |
| mz-171.026154631139 | 171.0262 | D-2-Hydroxyglutaric acid | M+Na | 0.3131 | RAMP_C_000219574 |
| mz-193.008454108564 | 193.0085 | D-2-Hydroxyglutaric acid | M+2Na-H | 0.3016 | RAMP_C_000219574 |
| mz-297.081385533322 | 297.0814 | D-2-Hydroxyglutaric acid | 2M+H | 0.1274 | RAMP_C_000219574 |
annotationInfo() records which engine and database
provenance downstream functions will consume.
annotation_info <- annotationInfo(annotated_object)
annotation_info_table <- data.frame(
field = names(annotation_info),
value = vapply(annotation_info, paste, collapse = "; ", character(1)),
row.names = NULL
)
knitr::kable(annotation_info_table, row.names = FALSE)| field | value |
|---|---|
| schema_version | 2 |
| engine | indexed-chemical-v2 |
| ramp_version | 3.0.7 |
| generated_at | 2026-10-14 02:12:09 UTC |
| candidates | 4 |
| assay | main |
| raw_mz_column | raw_mz |
| polarity | positive |
| maldi_matrix | unspecified |
| ppm | 5 |
| min_score | 0 |
| adducts | M+H; M+Na; M+2Na-H; 2M+H |
| database | pre-built annotation index |
The recommended production pattern is to retain all ppm-valid
candidates using min_score = 0, then select a threshold
appropriate to the analysis. This makes coverage and specificity tunable
without repeating index construction.
stored_candidates <- S4Vectors::metadata(annotated_object)$mz_annotation$results
score_thresholds <- c(0, 0.2, 0.5)
threshold_summary <- data.frame(
score_threshold = score_thresholds,
retained_candidates = vapply(
score_thresholds,
function(threshold) sum(stored_candidates$Score >= threshold),
integer(1)
)
)
knitr::kable(threshold_summary, row.names = FALSE)| score_threshold | retained_candidates |
|---|---|
| 0.0 | 4 |
| 0.2 | 3 |
| 0.5 | 0 |
For one call, annotateSM(db = NULL) automatically
resolves the current RaMP chemical-property table through
SpaMTPdb. For repeated calls, explicitly load the table,
then build and reuse an index. adducts is optional: when it
is omitted, SpaMTP selects the full rule space for the mode or MALDI
matrix and applies molecule-specific SMILES priors. An explicit list
remains useful when the acquisition protocol rules out known ion
families.
The smiles_features resource retrieved with
SpaMTPdb::spaMTPdbResource() stores these fields separately
from the pruned chem_props table and is joined by
iso_smiles on demand. For a custom database of up to 5,000
unique structures, infer_structure = "auto" parses missing
features at runtime. Larger un-precomputed inputs emit a warning instead
of silently adding a long preprocessing delay; use
infer_structure = "always", structure_workers,
or precompute with annotateSMILESStructure() when that cost
is intentional.
This section runs on all 2,075 measured mass features and 160 paired
observations in the bundled DHB mouse-brain demonstrator, with the
complete RaMP chemical table. The subset retains 80 locations per lesion
side around the archived ACB coordinate centroids, selected without
using expression or annotation results; see
help("vignetteData") and the workflow guide. It is evaluated by
default and downloads pinned annotation references if they are not
available locally or in a verified cache. The average MS1 spectrum
provides subset-level context, not pixel-specific validation of
identities.
database <- loadSpaMTPDatabase(c("chem_props", "source_df", "analytehaspathway",
"pathway", "ramp_db_metadata", "ramp_wikipathway", "ramp_reactome",
"ramp_kegg", "ramp_hmdb"), version = "3.0.7")
chem_props <- database$chem_props
object <- readRDS(system.file("extdata", "dhb_demo.rds", package = "SpaMTP"))
original_counts <- SummarizedExperiment::assay(object, "counts")
ms1 <- data.frame(mz = SummarizedExperiment::rowData(object)$mz,
intensity = Matrix::rowMeans(original_counts))
ramp_positive_index <- buildMZAnnotationIndex(
chem_props,
polarity = "positive", maldi_matrix = "DHB"
)
object <- annotateSM(
object,
db = NULL,
index = ramp_positive_index,
assay = "main",
raw.mz.column = "mz",
polarity = "positive", maldi_matrix = "DHB", ms1_spectrum = ms1,
ppm_error = 5,
min_score = 0,
return.only.annotated = FALSE
)## Searching candidate index at 5 ppm ...
## Adding annotations to rowData ....
stopifnot(identical(SummarizedExperiment::assay(object, "counts"), original_counts),
identical(dim(object), c(2075L, 160L)), nrow(chem_props) == 289754L)
annotationInfo(object)## $schema_version
## [1] 2
##
## $engine
## [1] "indexed-chemical-v2"
##
## $ramp_version
## [1] "3.0.7"
##
## $generated_at
## [1] "2026-10-14 02:14:44 UTC"
##
## $candidates
## [1] 29949
##
## $assay
## [1] "main"
##
## $raw_mz_column
## [1] "mz"
##
## $polarity
## [1] "positive"
##
## $maldi_matrix
## [1] "dhb"
##
## $ppm
## [1] 5
##
## $min_score
## [1] 0
##
## $adducts
## [1] "complete pre-built index rule space"
##
## $database
## [1] "pre-built annotation index"
Chemical eligibility and region characterization answer different
questions. Select candidates at a declared score threshold, exclude
ambiguous compound identities, and rank all retained
identities by descriptive regional effects. No pixel-level DE P values
are used for this single specimen. Mouse RNA remains in
altExp() but is not converted to human HGNC genes. Network
topology comes from human pathway resources; a compound match does not
validate mouse-specific regulatory interactions.
pathway_index <- buildPathwayIndex(database, gene_mapping = "ramp",
organism = "Mus musculus")
object <- normalizeSMData(object, "LogNormalize", verbose = FALSE)
object <- createPathwayAssay(object, assay = "main", slot = "logcounts",
new_assay = "compound", pathway_index = pathway_index, database = database,
organism = "Mus musculus", annotation_source = "current",
annotation_score_threshold = 0.05, metabolite_ambiguity = "exclude")
markers <- findAllDEMs(object, ident = "RegionLoupe", assay = "compound",
slot = "logcounts", method = "markers", spatial_blocks = 0)
ranks <- markers$expression
for (region in colnames(ranks)) {
tab <- markers$DEMs[markers$DEMs$cluster == region, ]
ranks[, region] <- tab$logFC[match(rownames(ranks), tab$gene)]
}
set.seed(1234)
regional_pathways <- findRegionalPathways(object, ident = "RegionLoupe",
ranks = list(metabolites = ranks), pathway_index = pathway_index,
min_path_size = 3, max_path_size = 100, organism = "Mus musculus")## | | | 0% | |==========================================================================================| 100%
stopifnot(nrow(regional_pathways) > 0L)
head(regional_pathways[, c("pathwayName", "Cluster_id", "NES", "padj")])## pathwayName Cluster_id NES padj
## 1 Metabolism ACB 1.327169 0.2442027
## 2 Metabolism of lipids ACB 1.254681 0.2712566
## 3 Signal Transduction ACB 1.112735 0.3826087
## 4 Disease ACB 1.490907 0.2052009
## 5 Infectious disease ACB 1.320296 0.2442027
## 6 Viral Infection Pathways ACB 1.281108 0.2442027
network_directory <- tempfile("dhb-annotation-networks-")
dir.create(network_directory)
network_file <- pathwayNetworkPlots(object, ident = "RegionLoupe",
regpathway = regional_pathways, DE.list = list(metabolites = markers$DEMs),
SM_assay = "compound", SM_slot = "logcounts", analyte_types = "metabolites",
database = database, pathway_index = pathway_index, organism = "Mus musculus",
annotation_score_threshold = 0.05, metabolite_detection = "annotated",
path = network_directory, image = NULL, top_n_pathways = 4, max_nodes = 350)## Selecting enriched pathways ...
## No stored topology was found for: Baseline metabolomic profiles: AC have prominent sugar metabolism whereas EC exhibit strong purine and amino acid metabolism; DHODH and cancer: promising prospects to be explored
## Preparing pathway edges once per topology ...
## Indexing differential results ...
## Extracting only plotted assay features ...
## Pathway network written to /tmp/Rtmp4y4zp6/dhb-annotation-networks-268020641637/RegionLoupe_2026_10_14_02_15_30.html
structure_evidence for important assignments.The native workflow illustrates spatial plotting and pathway-based prioritisation of stored candidates. These ranks remain hypotheses, not substitutes for orthogonal identification evidence.
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 26.04.1 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.32.so; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C LC_TIME=en_US.UTF-8
## [4] LC_COLLATE=en_US.UTF-8 LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C LC_ADDRESS=C
## [10] LC_TELEPHONE=C LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] SpaMTP_0.99.11 BiocStyle_2.41.0
##
## loaded via a namespace (and not attached):
## [1] splines_4.6.1 later_1.4.8 filelock_1.0.3
## [4] tibble_3.3.1 lifecycle_1.0.5 httr2_1.3.0
## [7] sf_1.1-3 edgeR_4.99.7 globals_0.20.0
## [10] lattice_0.23-1 crosstalk_1.2.2 magrittr_2.0.5
## [13] SpaMTPdb_0.99.4 limma_3.99.2 plotly_4.12.1
## [16] sass_0.4.10 rmarkdown_2.32 jquerylib_0.1.4
## [19] yaml_2.3.12 metapod_1.21.0 httpuv_1.6.17
## [22] otel_0.2.0 cowplot_1.2.0 DBI_1.3.0
## [25] buildtools_1.0.0 RColorBrewer_1.1-3 abind_1.4-8
## [28] GenomicRanges_1.65.4 purrr_1.2.2 BiocGenerics_0.59.12
## [31] rappdirs_0.3.4 SpaMTPData_0.99.5 IRanges_2.47.5
## [34] S4Vectors_0.51.10 ggrepel_0.9.8 irlba_2.4.1
## [37] listenv_1.1.0 maketools_1.3.2 pheatmap_1.0.13
## [40] units_1.0-1 RSpectra_0.16-2 matter_2.15.0
## [43] dqrng_0.4.1 parallelly_1.48.0 codetools_0.2-20
## [46] DelayedArray_0.39.8 scuttle_1.23.2 tidyselect_1.2.1
## [49] farver_2.1.2 ScaledMatrix_1.21.0 viridis_0.6.5
## [52] matrixStats_1.5.0 stats4_4.6.1 BiocFileCache_3.3.0
## [55] Seqinfo_1.3.2 jsonlite_2.0.0 CardinalIO_1.11.0
## [58] BiocNeighbors_2.7.3 e1071_1.7-17 scater_1.41.2
## [61] tools_4.6.1 Rcpp_1.1.2 glue_1.8.1
## [64] gridExtra_2.3.1 SparseArray_1.13.4 BiocBaseUtils_1.15.1
## [67] xfun_0.61 MatrixGenerics_1.25.0 dplyr_1.2.1
## [70] withr_3.0.3 BiocManager_1.30.27 fastmap_1.2.0
## [73] bluster_1.23.1 digest_0.6.39 rsvd_1.0.5
## [76] R6_2.6.1 mime_0.13 RSQLite_3.53.3
## [79] tidyr_1.3.2 generics_0.1.4 data.table_1.18.6.1
## [82] FNN_1.1.4.1 class_7.3-24 httr_1.4.9
## [85] htmlwidgets_1.6.4 S4Arrays_1.13.2 ontologyIndex_2.12
## [88] pkgconfig_2.0.3 gtable_0.3.6 blob_1.3.0
## [91] S7_0.2.2 SingleCellExperiment_1.35.2 XVector_0.53.0
## [94] sys_3.4.3 htmltools_0.5.9 fgsea_1.39.2
## [97] ProtGenerics_1.45.0 scales_1.4.0 Biobase_2.73.2
## [100] png_0.1-9 SpatialExperiment_1.23.0 scran_1.41.1
## [103] knitr_1.52 rjson_0.2.23 nlme_3.1-171
## [106] curl_8.0.0 proxy_0.4-29 cachem_1.1.0
## [109] stringr_1.6.0 Cardinal_3.15.0 BiocVersion_3.24.0
## [112] KernSmooth_2.23-27 parallel_4.6.1 vipor_0.4.7
## [115] AnnotationDbi_1.75.2 pillar_1.11.1 grid_4.6.1
## [118] vctrs_0.7.3 promises_1.5.0 BiocSingular_1.29.2
## [121] dbplyr_2.6.0 beachmat_2.29.3 xtable_1.8-8
## [124] cluster_2.1.8.3 beeswarm_0.4.0 evaluate_1.0.5
## [127] magick_2.9.1 cli_3.6.6 locfit_1.5-9.12
## [130] compiler_4.6.1 rlang_1.3.0 crayon_1.5.3
## [133] future.apply_1.20.2 labeling_0.4.3 mclust_6.1.3
## [136] classInt_0.4-11 naturalsort_0.1.3 ggbeeswarm_0.7.3
## [139] stringi_1.8.9 viridisLite_0.4.3 BiocParallel_1.47.0
## [142] Biostrings_2.81.9 Matrix_1.7-6 ExperimentHub_3.3.2
## [145] bit64_4.8.6 future_1.76.0 ggplot2_4.0.3
## [148] KEGGREST_1.53.6 statmod_1.5.2 shiny_1.14.0
## [151] SummarizedExperiment_1.43.0 AnnotationHub_4.3.2 igraph_2.3.4
## [154] memoise_2.0.1 bslib_0.12.0 fastmatch_1.1-8
## [157] bit_4.6.0