Skip to contents

Refining m/z Metabolite Annotations with SpaMTP

This tutorial highlights complementary ways to prioritise multiple candidate metabolites assigned to the same observed m/z. The first two approaches are implemented directly in SpaMTP. A custom matrix-adduct search can then extend the candidate space for known acquisition chemistry. Pseudo-MS/MS and paired targeted metabolomics provide additional independent evidence:

  1. Pathway-based prioritisation with CalculateAnnotationStatistics
  2. Lipid nomenclature simplification with RefineLipids
  3. Custom matrix-adduct design and single-peak candidate comparison
  4. Pseudo-MS/MS evidence from an independent MS1-ID/GNPS spectral database
  5. Comparison with paired targeted metabolomics data

These approaches provide complementary evidence. They should prioritise or support accurate-mass candidates rather than silently replace the original annotations.

Author: Andrew Causer


Install and Import R Libraries

First we need to import the required libraries for this analysis.

## Install SpaMTP if not previously installed
if (!require("SpaMTP"))
    devtools::install_github("GenomicsMachineLearning/SpaMTP")

#General Libraries
library(SpaMTP)
library(Cardinal)
library(Seurat)
library(dplyr)
library(tidyr)

#For plotting + DE plots
library(ggplot2)
library(EnhancedVolcano)
library(viridis)

1) Pathway-Based Refinement: CalculateAnnotationStatistics

This function uses correlations in pathway expression (Metabolite only, Gene only or Multi Omic based pathway information) to refine metabolite annotation. Specifically, for each annotated metabolite, the expression profiles for all known pathways associated with this metabolite are calculated. Based on spatial colocalisation between relative pathways and the given m/z mass, each metabolite is then ranked based on Pearson correlation values and the number of significant pathways associated with that metabolite.

Below we will use a public mouse liver dataset with spotted chemicals standards to demonstrate this:

spotted_file <- resolve_spamtp_vignette_file(
  article = "Annotation_Refinement",
  filename = "spotted_Annotated_H_Cl_Adducts.RDS",
  url = "https://zenodo.org/api/records/17289187/files/spotted_Annotated_H_Cl_Adducts.RDS/content",
  expected_md5 = "d7b46c8d71b7a3eb95a01a210e010c40"
)
spotted <- readRDS(spotted_file)
spotted
## An object of class Seurat 
## 1303 features across 109390 samples within 1 assay 
## Active assay: Spatial (1303 features, 0 variable features)
##  1 layer present: counts
##  1 spatial field of view present: fov

Here we have a SpaMTP Seurat object that is already processed, with m/z annotated. Meaning we have 1303 annotated m/z values. This dataset includes certain chemical compounds which were ‘spiked-in’ at known locations. We will plot these below:

metabolite_colors <- list("ATP" = "azure","ADP" = "green","Adenosine" = "yellow", "Guanine" = "red","Glutamic Acid" = "turquoise1", "Glutamine" = "chocolate1","Glutathione" = "deeppink","liver" = "black")

ImageDimPlot(spotted, group.by = "clusters", cols = metabolite_colors, dark.background = F)

The plot highlights the location of each known metabolite. However, due to the inherent issue of mass spectrometry imaging data one m/z mass may be associated with multiple possible metabolites. For example adenosine triphosphate or ATP was spiked-in to the second set of wells within the liver (green). When we look at the annotation of ATP, we can see for m/z-505.988470137 there are 4 different annotations, one of which is ATP.

SearchAnnotations(spotted, metabolite = "Adenosine triphosphate", search.exact = T)
raw_mz mz_names observed_mz all_IsomerNames all_Isomers all_Isomers_IDs all_Adducts all_Formulas all_Errors
1162 505.9885 mz-505.988470137 505.9885 Adenosine triphosphate; dGTP; 2-hydroxy-dATP; [[(2R,5R)-5-(6-Aminopurin-9-yl)-3,4-dihydroxyoxolan-2-yl]methoxy-hydroxyphosphoryl] phosphono hydrogen phosphate HMDB0000538; HMDB0001440; HMDB0059593; HMDB0257997 hmdb:HMDB0000538; hmdb:HMDB0001440; hmdb:HMDB0059593; hmdb:HMDB0257997 M-H C10H16N5O13P3 0.0019

Plotting this m/z value spatially, it localises completely within the ATP spiked-in region.

ImageDimPlot(spotted, group.by = "clusters", cols = setNames(
  ifelse(names(metabolite_colors) == "ATP", "azure", "darkgrey"),names(metabolite_colors)
))+ ggtitle("ATP")| ImageMZPlot(spotted, mzs = 505.9885)

This confirms that the correct annotation of mz-505.988470137 is only ‘ATP’. This is the case with all other spiked-in chemicals. To refine the annotations based on pathway correlation we can run SpaMTP’s CalculateAnnotationStatistics:

## Step 1: Create a pathway assay - This can be done using only metabolites, or can be merged to include genes if applicable
spotted <- CreatePathwayAssay(spotted, analyte_type = "metabolites", assay = "Spatial", new_assay = "SPM_pathway", verbose = T)

## Step 2: Format Pathway Assay
DefaultAssay(spotted) <- "SPM_pathway"
spotted <- NormalizeData(spotted)
spotted <- ScaleData(spotted)

## Step 3: Run CalculateAnnotationStatistics
output <- CalculateAnnotationStatistics(data = spotted, mz.assay = "Spatial",pathway.assay="SPM_pathway", mz.slot = "counts", return.top = TRUE)

After running CalculateAnnotationStatistics the output will be a dataframe containing the most likely annotation for each m/z value based on correlations with pathway expression. Lets subset this output to look at our spiked-in metabolites:

output[c(1162,1053,501,626,126,117,251,113,248,637,638,835,836),]
raw_mz mz_names metabolite n_sig_path max_cor z_score pval pval_adj original_annotations
1162 505.9885 mz-505.988470137 Adenosine triphosphate 2381 0.8374779 1.731981 0.0416385 0.1249154 Adenosine triphosphate; dGTP; 2-hydroxy-dATP; [[(2R,5R)-5-(6-Aminopurin-9-yl)-3,4-dihydroxyoxolan-2-yl]methoxy-hydroxyphosphoryl] phosphono hydrogen phosphate
1053 426.0221 mz-426.022139245 ADP 3579 0.8985871 2.541858 0.0055132 0.0275662 Adenosine 3’,5’-diphosphate; dGDP; ADP; [(2S,3R,4R,5R)-5-(6-Aminopurin-9-yl)-3,4-dihydroxyoxolan-2-yl]methyl phosphono hydrogen phosphate; Zidovudine diphosphate
501 266.0895 mz-266.089477461 Adenosine 104 0.9071216 3.756311 0.0000862 0.0006897 3’-Azido-3’-deoxythymidine, 98%; 8-Oxo-2’-deoxyadenosine; Adenosine; Deoxyguanosine; Vidarabine; Zidovudine; 9-Arabinofuranosyladenine; Formycin A
626 302.0662 mz-302.066155193 Adenosine 104 0.8019030 4.117214 0.0000192 0.0001726 Hpmpa; 3’-Azido-3’-deoxythymidine, 98%; 8-Oxo-2’-deoxyadenosine; Adenosine; Deoxyguanosine; Vidarabine; Zidovudine; 9-Arabinofuranosyladenine; Formycin A
126 150.0421 mz-150.042133345 Guanine 42 0.6453926 3.000000 0.0013499 0.0053996 8-Oxo-7,8-dihydrodeoxyguanine; Guanine; 2-Hydroxyadenine; 8-Hydroxyadenine
117 146.0459 mz-146.045881333 L-Glutamic acid 373 0.8193504 4.792862 0.0000008 0.0000099 N-lactoyl-Glycine; 5-Hydroxy-4-oxo-L-norvaline; Alanine acetate; N-Methyl-DL-aspartic acid; L-Glutamic acid; N-Methyl-D-aspartic acid; N-Acetylserine; O-Acetylserine; D-Glutamic acid; L-4-Hydroxyglutamate semialdehyde; 3-(Carboxymethylamino)propanoic acid; DL-Glutamate
251 182.0226 mz-182.022559065 L-Glutamic acid 343 0.2866179 5.439546 0.0000000 0.0000004 Fosmidomycin; 2-Amino-4-phosphonobutyric acid; N-lactoyl-Glycine; 5-Hydroxy-4-oxo-L-norvaline; Alanine acetate; N-Methyl-DL-aspartic acid; L-Glutamic acid; N-Methyl-D-aspartic acid; N-Acetylserine; O-Acetylserine; D-Glutamic acid; L-4-Hydroxyglutamate semialdehyde; 3-(Carboxymethylamino)propanoic acid; DL-Glutamate
113 145.0619 mz-145.061865745 L-Glutamine 137 0.5805269 4.103823 0.0000203 0.0001626 Alanylglycine; Cyclic Urea; Glycyl-D-Alanine; Glycylsarcosine; D-Alanyl glycine; L-Glutamine; Ureidoisobutyric acid; D-Glutamine; 3-ureido-isobutyrate
248 181.0385 mz-181.038543477 L-Glutamine 137 0.5005050 4.126675 0.0000184 0.0001472 Alanylglycine; Cyclic Urea; Glycyl-D-Alanine; Glycylsarcosine; D-Alanyl glycine; L-Glutamine; Ureidoisobutyric acid; D-Glutamine; 3-ureido-isobutyrate
637 306.0765 mz-306.076529999 Glutathione 175 0.7969612 4.082483 0.0000223 0.0001337 Glutathione; Glutathionate(1-); Methyl 2-((3,4-dihydro-3,4-dioxo-1-naphthalenyl)amino)benzoate; Hallacridone; Aristolodione; zaprinast
638 306.0772 mz-306.077181461 Glutathione 175 0.7969960 4.082483 0.0000223 0.0001337 Glutathione; Glutathionate(1-); Methyl 2-((3,4-dihydro-3,4-dioxo-1-naphthalenyl)amino)benzoate; Hallacridone; Aristolodione; zaprinast
835 342.0532 mz-342.053207731 Glutathione 175 0.5391956 4.082483 0.0000223 0.0001337 O-Desmethylindomethacin; Glutathione; Glutathionate(1-); Methyl 2-((3,4-dihydro-3,4-dioxo-1-naphthalenyl)amino)benzoate; Hallacridone; Aristolodione
836 342.0539 mz-342.053859193 Glutathione 175 0.5441561 4.082483 0.0000223 0.0001337 O-Desmethylindomethacin; Glutathione; Glutathionate(1-); Methyl 2-((3,4-dihydro-3,4-dioxo-1-naphthalenyl)amino)benzoate; Hallacridone; Aristolodione

We can see for each spiked-in metabolite the predicted metabolite with the highest significance in each case corresponded to the spike-in. This method gives the user higher confidence that the annotations of m/z value are correct and biologically significant. Note: For large datasets this function can take a while to run, the function CalculateSingleAnnotationStatistics can be used instead to calculate refined annotations for a single m/z value. Please visit the documentation for more information.

2) Lipid Nomenchlature Refinement

As described previously in the Spatial Metabolomics Analysis tutorial, because many different kinds of lipids can share the same molecular weight, this may results in some m/z values possessing many possible annotations. The SpaMTP function RefineLipids can be used to simplify the lipid nomenclature into common lipid categories and classes, giving more simplified annotations to a level that most SM datasets are capable of identifying.

### Runs lipid nomenclature simplification on annotations
refined_lipid_annotations <- RefineLipids(spotted@assays$Spatial@meta.data, annotation.column = "all_IsomerNames", lipid_info = "simple")

We can count the number of metabolites assigned for each m/z before and after refining the lipid annotations.

## Count original number of annotations per m/z 
refined_lipid_annotations <- refined_lipid_annotations %>%
  mutate(base_count = sapply(strsplit(all_IsomerNames, ";\\s*"), length))

## Count number of annotations per m/z following lipid simplification
refined_lipid_annotations <- refined_lipid_annotations %>%
  mutate(
    refined_count = ifelse(
      is.na(Lipid.Maps.Category),
      base_count,
      sapply(strsplit(Lipid.Maps.Category, ";\\s*"), length)
    )
  )

The maximum reduction in annotations was from 161 to 1. This is quite a significant simplification which can help in processing large datasets. Lets visualise the difference between each m/z value below:

# reshape to long format
df_long <- refined_lipid_annotations %>%
  select(raw_mz, refined_count, base_count) %>%
  pivot_longer(cols = c(refined_count, base_count),
               names_to = "Type", values_to = "Count")

# plot
ggplot(df_long, aes(x = raw_mz, y = Count, color = Type)) +
  geom_point(size = 2, alpha = 0.7) +
  geom_smooth(method = "lm", se = FALSE, size = 1.2) +
  scale_color_manual(values = c("refined_count" = "red",
                                "base_count" = "blue"),
                     labels = c("Base annotations", "Refined annotations")) +
  labs(
    x = expression(italic("m/z")),
    y = "Number of annotations",
    color = ""
  ) +
  theme_minimal(base_size = 14) +
  theme(
    legend.position = "top",
    panel.grid.minor = element_blank(),
    axis.title = element_text(face = "bold")
  )

We can see the larger m/z values, which correspond mostly to lipids, display the biggest differences following annotation refinement.

3) Single-Peak Comparison: Custom Matrix-Adduct Design

The standard adduct search space is appropriate only when its ion-formation rules reflect the experiment. Reactive matrices such as FMP-10 change the analyte before ion detection. The original FMP-10 workflow represented these matrix-derived products in the annotation Adduct field, which is a useful computational abstraction even though FMP-10 derivatization is a covalent reaction followed by possible gas-phase chemistry, rather than a conventional non-covalent adduct.

[M+2FMP10a]+ has a 521.202 Da shift and one charge

The mass of the purchased reagent must not be added directly to the analyte. FMP-10 is supplied as the iodide salt, C20H15FIN, with an average molar mass of approximately 415.25 g/mol. The iodide is a counter-ion and is not retained in the detected dopamine derivative. Even the active FMP-10 cation, C20H15FN+ (approximately 288.12 Da), is not the net tag mass: during nucleophilic aromatic substitution, fluorine leaves from FMP-10 and a hydrogen is lost from the reacting analyte group.

The mass bookkeeping for product I (2FMP10a) is therefore:

Stage Composition used for the mass change Approximate monoisotopic change Charge interpretation
Purchased FMP-10 iodide salt C20H15FIN Not used as an adduct shift Salt contains a cation and iodide counter-ion.
One covalently retained tag C20H15FN - F - H = C20H14N 268.11262 Da One pyridinium charge is introduced.
Two retained tags 2(C20H14N) = C40H28N2 536.22525 Da The initial double derivative contains two permanent charges.
MALDI product I C40H28N2 - CH3 = C39H25N2 521.202 Da (reported, rounded) Gas-phase demethylation/charge neutralisation gives the observed singly charged ion.

Thus, 2FMP10 describes two derivatization events, whereas the suffix a identifies one final ion product from those events. It does not mean 2 * 288.12 Da, and z = +1 describes the final ion reaching the detector, not the charge of the transient double derivative. A second reported product, 2FMP10b, follows an alternative dehydrogenation route and has a different net shift (approximately 535.218 Da). The original study establishes the FMP-10 covalent charge-tagging chemistry and the dopamine signal near m/z 674.28; the subsequent reaction study explicitly describes the singly charged demethylated and dehydrogenated double derivatives (original FMP-10 study; reaction-product study).

For transparent computation, the rule below derives the product-I shift from monoisotopic elemental masses. Following the mass-shift convention used in the reaction study, the elemental composition change and the final charge are stored separately. C39H25N2 gives 521.20177 Da, which is reported as 521.202 Da after rounding.

fmp10_element_mass <- c(
  C = 12,
  H = 1.00782503223,
  N = 14.00307400443
)

# Each substitution retains C20H14N from the active FMP-10 cation.
fmp10_retained_tag_mass <-
  20 * fmp10_element_mass[["C"]] +
  14 * fmp10_element_mass[["H"]] +
  fmp10_element_mass[["N"]]

# Product I: two retained tags, followed by loss of CH3.
# The final +1 charge is recorded separately in the rule table below.
fmp10_double_a_shift <-
  2 * fmp10_retained_tag_mass -
  (fmp10_element_mass[["C"]] + 3 * fmp10_element_mass[["H"]])

fmp10_mass_accounting <- data.frame(
  quantity = c(
    "One retained FMP-10 tag",
    "Two retained tags before demethylation",
    "Final 2FMP10a shift used by SpaMTP"
  ),
  monoisotopic_mass_Da = c(
    fmp10_retained_tag_mass,
    2 * fmp10_retained_tag_mass,
    fmp10_double_a_shift
  )
)

knitr::kable(fmp10_mass_accounting, digits = 7, row.names = FALSE)
quantity monoisotopic_mass_Da
One retained FMP-10 tag 268.1126
Two retained tags before demethylation 536.2252
Final 2FMP10a shift used by SpaMTP 521.2018

This exact mass is a candidate-generation rule, not proof that a peak is dopamine. Its credibility comes from restricting the search space with known experimental chemistry. A defensible FMP-10 search should proceed in the following order:

  1. Search all RaMP compounds with the ordinary positive-mode adduct rules.
  2. Add FMP-10 product rules only when the sample was actually prepared with FMP-10.
  3. Require enough compatible reaction sites for each rule. For 2FMP10a, the candidate should have at least two validated FMP-10-reactive sites (phenolic hydroxyl and/or primary or secondary amine sites). Dopamine satisfies this requirement.
  4. Compare the calculated product m/z with the observed m/z under the chosen ppm tolerance, while retaining all mass-compatible alternatives.
  5. Award additional confidence for independent evidence such as the expected FMP-10 product family, deuterated-FMP-10 shifts, isotope patterns, spatial co-localisation, pseudo-MS/MS, or an authentic standard.

If reactive-site counts cannot be obtained from a structure, SpaMTP should retain the mass match as an exploratory candidate but mark the chemistry as unverified and lower its prior. In other words, expanding the search space earns evidential credit only when the acquisition matrix, reaction-site eligibility, exact mass, and final charge are all recorded; merely adding a large arbitrary mass shift does not.

This distinction matters for candidate generation. For example, the FMP-10 mouse-brain demonstration contains an observed peak at m/z 674.2805, assigned to double-derivatised dopamine, [M+2FMP10a]+. Dopamine has a neutral monoisotopic mass of approximately 153.07898 Da. None of the standard positive adduct rules place dopamine near the observed peak:

dopamine_peak_mz <- 674.2805

dopamine_db <- chem_props[
  toupper(chem_props$chem_source_id) == "HMDB:HMDB0000073",
  c(
    "ramp_id", "chem_source_id", "inchi_key", "monoisotop_mass",
    "common_name", "mol_formula"
  ),
  drop = FALSE
]
dopamine_db <- dopamine_db[1L, , drop = FALSE]
stopifnot(nrow(dopamine_db) == 1L)

# Dopamine has two phenolic hydroxyls and one primary amine. This explicit
# structure-derived field lets matrix-reaction rules prune impossible products.
dopamine_db$fmp10_reactive_sites <- 3L

standard_positive_rules <- AdductRules("positive")
standard_dopamine_index <- BuildMZAnnotationIndex(
  dopamine_db,
  polarity = "positive",
  rules = standard_positive_rules
)
standard_dopamine_hits <- QueryMZAnnotationIndex(
  dopamine_peak_mz,
  standard_dopamine_index,
  ppm = 5
)

data.frame(
  observed_mz = dopamine_peak_mz,
  search_space = "Standard positive adducts",
  dopamine_matches = nrow(standard_dopamine_hits)
) |>
  knitr::kable(digits = 7, row.names = FALSE)
observed_mz search_space dopamine_matches
674.2805 Standard positive adducts 0

FMP-10 chemistry provides the missing information. Double derivatization can produce different singly charged products with reported net shifts of approximately 521.202 and 535.218 Da. These are distinct reaction-product rules; they should not be generated by blindly multiplying either the salt, active-cation, or single-tag mass. SpaMTP selects the appropriate product rules from the declared matrix profile. The user does not need to supply adducts; that argument is only needed to restrict the automatic search space. The matrix profile extends the standard rules without changing the RaMP compound database:

fmp10_rules <- MALDIMatrixRules(
  maldi_matrix = "FMP-10",
  polarity = "positive"
)

fmp10_product_rules <- fmp10_rules[
  fmp10_rules$rule_class == "reactive_product",
  c(
    "name", "mass_shift", "charge", "reactive_group",
    "min_reactive_sites", "rule_source"
  ),
  drop = FALSE
]
stopifnot(
  isTRUE(all.equal(
    fmp10_product_rules$mass_shift[
      fmp10_product_rules$name == "M+2FMP10a"
    ],
    fmp10_double_a_shift,
    tolerance = 1e-10
  ))
)

knitr::kable(fmp10_product_rules, digits = 7, row.names = FALSE)
name mass_shift charge reactive_group min_reactive_sites rule_source
M+FMP10 268.1126 1 FMP-10-reactive site 1 10.1038/s41592-019-0551-3; 10.1002/cmtd.202500062
M+2FMP10a 521.2018 1 FMP-10-reactive site 2 10.1038/s41592-019-0551-3; 10.1002/cmtd.202500062
M+2FMP10b 535.2180 1 FMP-10-reactive site 2 10.1038/s41592-019-0551-3; 10.1002/cmtd.202500062
# `adducts` is intentionally omitted: use every rule selected by FMP-10.
fmp10_dopamine_index <- BuildMZAnnotationIndex(
  dopamine_db,
  polarity = "positive",
  maldi_matrix = "FMP-10"
)
fmp10_dopamine_hits <- QueryMZAnnotationIndex(
  dopamine_peak_mz,
  fmp10_dopamine_index,
  ppm = 5
)

knitr::kable(
  fmp10_dopamine_hits[, c(
    "observed_mz", "expected_mz", "ppm_error", "adduct", "neutral_mass",
    "metabolite_names", "ramp_ids", "reactive_site_status", "score"
  )],
  digits = 7,
  row.names = FALSE
)
observed_mz expected_mz ppm_error adduct neutral_mass metabolite_names ramp_ids reactive_site_status score
674.2805 674.2808 0.3743479 M+2FMP10a 153.079 Dopamine RAMP_C_000218860 verified 0.7313182

Here mass_shift and charge describe the net detected ion. The generic rule fields enforce the final charge balance expected by the annotation engine; they do not claim that no hydrogen was lost during the two substitution reactions. Those reaction-site hydrogen losses, the intermediate +2 state, and the subsequent -CH3 transformation are already folded into the product rule. Because fmp10_reactive_sites = 3 was provided, the double derivative is reported as verified for reaction-site eligibility. If this structure-derived value is unavailable, SpaMTP retains the candidate as unknown but applies a 0.25 score multiplier instead of silently treating its chemistry as verified.

The custom rule recovers dopamine at an expected m/z of approximately 674.28075, within 1 ppm of the observed peak. This is a targeted comparison of the dopamine hypothesis: it does not imply that no other RaMP compound could match m/z 674.2805 through a standard or custom rule. A full untargeted analysis must query the complete RaMP index and report the competing candidates rather than search dopamine alone.

The rule table is deliberately separate from chem_props. A reusable custom rule should record the reactive matrix, net mass shift, charge, product variant, polarity, source publication, and a calibrated prior. Apply it only to experiments using the corresponding chemistry. FMP-10 also supports single and other multiple-derivatization products, so each validated reaction product requires its own rule. The FMP-10 reaction and its use for neurotransmitter MSI are described in the original study; the alternative double-derivatization mass shifts are detailed in a later reaction study.

This example illustrates the different roles of the evidence layers:

Evidence layer Interpretation for m/z 674.2805
Standard adduct search Dopamine is outside the standard positive-mode search space.
Custom FMP-10 rule The known matrix chemistry makes double-derivatised dopamine a mass-compatible candidate.
Pseudo-MS/MS An independent spectral match can support or challenge that candidate, but should not be fabricated from the adduct rule.

The existing curated-panel interface remains useful when reference product masses were measured directly. For example, AddFMP10Annotations(..., only.fmp.adduct = TRUE) applies SpaMTP’s curated FMP-10 product table. The profile/rule approach above is preferable when designing a transparent, extensible search space for a new reactive matrix or product variant.

Automatic matrix profiles and optional adduct selection

The same API covers conventional and reactive MALDI preparation. In the usual case, the user supplies the experimental matrix and polarity but does not need to enumerate adducts:

annotated <- AnnotateSM(
  object,
  assay = "Spatial",
  maldi_matrix = "DHB",
  polarity = "positive",
  ppm_error = 5
)

# Optional restriction for a targeted analysis; not a compulsory argument.
targeted <- AnnotateSM(
  object,
  assay = "Spatial",
  maldi_matrix = "FMP-10",
  polarity = "positive",
  adducts = c("M+FMP10", "M+2FMP10a"),
  ppm_error = 5
)

MALDIMatrixProfiles() is the auditable registry. It includes widely used conventional matrices, reactive matrices, and common on-tissue derivatization reagents, together with the target functional groups and automatic-rule status:

matrix_profiles <- MALDIMatrixProfiles()
knitr::kable(
  matrix_profiles[, c(
    "display_name", "category", "default_polarity", "target_groups",
    "automatic_rules"
  )],
  row.names = FALSE
)
display_name category default_polarity target_groups automatic_rules
No specified MALDI matrix unspecified positive none standard
2,5-Dihydroxybenzoic acid (DHB) conventional_matrix positive none validated_matrix_adduct
alpha-Cyano-4-hydroxycinnamic acid (CHCA) conventional_matrix positive none validated_matrix_adduct
9-Aminoacridine (9-AA) conventional_matrix negative none standard
1,5-Diaminonaphthalene (DAN) conventional_matrix negative none standard
Norharmane conventional_matrix both none standard
FMP-10 reactive_matrix positive primary/secondary amine; phenolic hydroxyl validated_reactive_product
FMP-8 reactive_matrix positive primary/secondary amine; phenolic hydroxyl profile_only
FMP-9 reactive_matrix positive primary/secondary amine; phenolic hydroxyl profile_only
2,4-Diphenylpyrylium tetrafluoroborate (DPP-TFB) reactive_matrix positive primary amine profile_only
2,4,6-Trimethylpyrylium tetrafluoroborate (TMP-TFB) reactive_matrix positive primary amine profile_only
N-Methylpyridinium boronic acid (N-MePyBA) reactive_matrix positive catechol/1,2-diol profile_only
2,4-Dinitrophenylhydrazine (DNPH) reactive_matrix positive aldehyde/ketone profile_only
Coniferyl aldehyde (CA) reactive_matrix positive primary amine profile_only
2,4-Dihydroxybenzaldehyde (DHBA) reactive_matrix positive primary amine profile_only
2,5-Dihydroxyacetophenone (DHAP) reactive_matrix positive primary amine profile_only
Girard reagent T otcd_reagent positive aldehyde/ketone profile_only
Girard reagent P otcd_reagent positive aldehyde/ketone profile_only
2-Picolylamine (2-PA) otcd_reagent positive carboxylic acid profile_only
N,N,N-Trimethyl-2-(piperazin-1-yl)ethanaminium (TMPA) otcd_reagent positive carboxylic acid profile_only
AMPP/HATU otcd_reagent positive carboxylic acid; aldehyde profile_only
TAHS otcd_reagent positive catecholamine profile_only

The profiles deliberately distinguish ionisation behaviour from covalent reaction chemistry:

Matrix/profile Automatic behaviour
DHB Uses the standard positive-ion space and adds validated dehydrated-DHB matrix adducts such as [M+(DHB-H2O)+H]+. Because DHB also produces abundant matrix clusters, an isolated mass match receives a low prior and should be supported by its parent ion and spatial correlation.
CHCA/HCCA Uses the standard positive-ion space and adds the reported major [M+CHCA+Na]+ matrix-adduct rule. It is treated as a non-covalent matrix adduct, not a universal covalent tag.
9-AA Defaults to negative mode and ordinary negative ions, especially [M-H]-. No fixed M+9AA product is generated because a universal 9-AA metabolite-product shift is not established.
FMP-10 Adds covalent single- and double-derivative products with reaction-site requirements and literature provenance.
Other registered reactive matrices/reagents FMP-8/9, DPP-TFB, TMP-TFB, N-MePyBA, DNPH, coniferyl aldehyde, DHBA, DHAP, Girard T/P, 2-picolylamine, TMPA, AMPP/HATU, and TAHS are represented. A profile_only entry selects a sensible polarity and standard ion space but does not invent a universal product mass; a verified study-specific rule can be supplied explicitly.

For DHB and CHCA matrix adducts, providing ms1_spectrum activates the existing adduct-family check. SpaMTP searches for the corresponding base ion (for example [M+H]+ or [M+Na]+) in the same spectrum and multiplies an unsupported matrix-adduct candidate by 0.1. In MSI, the stronger validation is per-pixel ion-image correlation between the parent and proposed matrix-adduct feature; that spatial evidence should be stored alongside the annotation when available.

4) Pseudo MS/MS-Based Refinement

Rationale

Full-scan MSI data do not contain isolated product-ion spectra. However, an intact ion, its alternative ion forms, and its in/post-source fragments can have similar spatial distributions. A precursor-specific pseudo-MS/MS spectrum can therefore be reconstructed by grouping m/z features with correlated ion images and comparing the resulting spectrum with a reference MS/MS library.

Importantly, the mean spectrum across all pixels is not a precursor-specific pseudo-MS/MS spectrum. Pooling every detected feature in this way can combine unrelated metabolites and inflate spectral matches. The workflow used here is based on the published MS1-ID method and its reference implementation:

  1. Detect and denoise m/z features from the imzML data.
  2. Remove features without sufficient spatial structure or pixel coverage.
  3. Calculate ion-image correlations and group co-localised features.
  4. Construct one pseudo-MS/MS spectrum for each retained target m/z.
  5. Search the spectra against unscaled and intensity-scaled GNPS libraries using precursor-tolerant reverse spectral matching.
  6. Retain the score, matched-peak count, spectral usage, InChIKey, library identifier, and all processing parameters as annotation evidence.

Reverse matching ignores additional unmatched peaks in the experimental pseudo-spectrum, which may originate from correlated background compounds. It does not make the reconstructed spectrum equivalent to experimentally acquired MS/MS, and it generally cannot distinguish structural isomers with similar fragmentation.

Keep RaMP and pseudo-MS/MS results independent

SpaMTP treats the two resources as separate annotation layers:

Annotation layer Primary question Recommended storage
RaMP accurate-mass candidates Which neutral compounds and adducts are compatible with the observed m/z? The scored SpaMTP annotation table in object@tools$mz_annotation
MS1-ID/GNPS pseudo-MS/MS evidence Which library structures are supported by spatially correlated ion forms and fragments? A separate, versioned result table or RDS database

The GNPS spectral library is therefore not bundled into the RaMP database and pseudo-MS/MS scores do not overwrite the RaMP mass, isotope, chemical-validity, or adduct-network scores. Keeping the results independent also allows either database to be updated without rebuilding the other. Candidate agreement can later be assessed using stable identifiers such as InChIKey; compound names alone should not be used for this comparison.

Generate the independent pseudo-MS/MS database

MS1-ID is an optional external Python workflow and is not installed or executed automatically by SpaMTP. The input directory must contain matching .imzML and .ibd files. The following example uses both official GNPS library variants:

ms1_id msi \
  --input_dir path/to/msi_data \
  --libs db/gnps_minmz100.pkl db/gnps_minmz100_k10.pkl \
  --mode positive \
  --mz_ppm_tol 5 \
  --min_feature_spatial_chaos 0.10 \
  --min_pixel_overlap 50 \
  --min_correlation 0.85 \
  --lib_search_mztol 0.05 \
  --score_cutoff 0.70 \
  --min_matched_peak 3 \
  --min_spec_usage 0.05 \
  --n_cores 12

These values are starting points, not universal confidence boundaries. Mass accuracy, spatial resolution, the number of tissue pixels, ionisation mode, and instrument type should guide parameter selection. In particular, min_pixel_overlap must be reduced cautiously for small datasets, and a lower score threshold increases exploratory coverage at the cost of more false matches.

For each imzML file, MS1-ID writes ms1_id_annotations_all.tsv and a dereplicated ms1_id_annotations_derep.tsv. The complete table should be retained because it preserves alternative spectral candidates; the dereplicated table is convenient for inspection but should not be the only archived result.

pseudo_msms_file <- file.path(
  "path/to/msi_data",
  "sample_name",
  "ms1_id_annotations_all.tsv"
)

pseudo_msms_results <- read.delim(
  pseudo_msms_file,
  sep = "\t",
  stringsAsFactors = FALSE,
  check.names = FALSE
)

required_columns <- c(
  "pms2_idx", "name", "mz", "spatial_chaos", "matched_score",
  "matched_peak", "spectral_usage", "precursor_mz", "precursor_type",
  "formula", "inchikey", "db_name", "db_id", "pseudo_ms2"
)
stopifnot(all(required_columns %in% colnames(pseudo_msms_results)))

Thresholds can be changed by the user when analysing the retained table, provided that the original MS1-ID search was run with equally permissive or more permissive cut-offs:

pseudo_score_threshold <- 0.70
minimum_matched_peaks <- 3L
minimum_spectral_usage <- 0.05

selected_pseudo_msms <- pseudo_msms_results |>
  dplyr::filter(
    matched_score >= pseudo_score_threshold,
    matched_peak >= minimum_matched_peaks,
    spectral_usage >= minimum_spectral_usage
  ) |>
  dplyr::arrange(mz, dplyr::desc(matched_score),
                 dplyr::desc(matched_peak))

For the same dopamine peak, the pseudo-MS/MS table can be queried independently by observed precursor m/z and InChIKey. The first 14 InChIKey characters compare the two-dimensional molecular connectivity while avoiding an accidental name match:

dopamine_connectivity_key <- substr(
  toupper(dopamine_db$inchi_key[[1]]),
  1,
  14
)

dopamine_pseudo_msms <- selected_pseudo_msms |>
  dplyr::mutate(
    observed_ppm_error = abs(mz - dopamine_peak_mz) /
      dopamine_peak_mz * 1e6,
    connectivity_key = substr(toupper(inchikey), 1, 14)
  ) |>
  dplyr::filter(
    observed_ppm_error <= 5,
    connectivity_key == dopamine_connectivity_key
  ) |>
  dplyr::arrange(
    dplyr::desc(matched_score),
    dplyr::desc(matched_peak)
  )

dopamine_pseudo_msms[, c(
  "mz", "name", "matched_score", "matched_peak", "spectral_usage",
  "precursor_type", "inchikey", "db_name", "db_id"
)]

An empty result means that the independent pseudo-MS/MS layer did not support dopamine under the selected thresholds; it must not erase the custom-adduct candidate. Conversely, a spectral match supports the proposed structure but does not validate the FMP-10 reaction rule by itself. Keeping both rows of evidence preserves that distinction.

The result and its provenance should be saved together as an independent database. Record the installed MS1-ID version, GNPS release or checksum, input file checksum, ionisation mode, and all thresholds used for the analysis.

pseudo_msms_database <- list(
  metadata = list(
    engine = "ms1_id",
    engine_version = "record the installed version",
    spectral_library = c(
      "gnps_minmz100.pkl",
      "gnps_minmz100_k10.pkl"
    ),
    ion_mode = "positive",
    mz_ppm_tolerance = 5,
    minimum_spatial_correlation = 0.85,
    generated_at = format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z")
  ),
  results = pseudo_msms_results
)

saveRDS(pseudo_msms_database, "sample_name_pseudo_msms_database.rds")

Interpretation and validation

A pseudo-MS/MS match should be reported as supporting evidence for a candidate, not as proof of identity. At minimum, inspect the reverse-match score, number of matched peaks, spectral usage, spatial coherence of the matched ions, precursor/adduct agreement, and library provenance. Lack of a match is also not evidence that a RaMP candidate is absent: some metabolites generate few in-source fragments or are poorly represented in public libraries.

Whenever possible, calibrate the thresholds using authentic standards or spiked compounds measured on the same platform. Pixel-permuted ion images and decoy spectral libraries provide useful negative controls. Final reporting should distinguish accurate-mass candidates, pseudo-MS/MS-supported candidates, and identifications confirmed by authentic MS/MS or standards.

5) Refinement with Paired Targeted Metabolic Data

To come!