Install any skill in seconds. Free to start, no credit card required.
Get Started Free →MS spectral matching and metabolite ID with matchms. Import spectra (mzML, MGF, MSP, JSON), filter/normalize peaks, score similarity (cosine, modified cosine, fingerprint), build reproducible pipelines, identify unknowns vs spectral libraries. Use pyopenms for full LC-MS/MS proteomics.
.claude/skills/jaechang-hits-matchms-spectral-matching/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 124% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 146% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 128% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 240% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 179% | 0% |
Matchms is a Python library for mass spectrometry data processing focused on spectral similarity calculation and compound identification. It provides multi-format I/O, 50+ spectrum filters for metadata harmonization and peak processing, 8 similarity scoring functions, and a pipeline framework for reproducible analytical workflows.
bashuv pip install matchms numpy pandas # For chemical structure processing (SMILES, InChI, fingerprints): uv pip install matchms[chemistry]
pythonfrom matchms.importing import load_from_mgf from matchms.filtering import default_filters, normalize_intensities from matchms.filtering import select_by_relative_intensity, require_minimum_number_of_peaks from matchms import calculate_scores from matchms.similarity import CosineGreedy # Load and process query spectra queries = list(load_from_mgf("queries.mgf")) queries = [default_filters(s) for s in queries] queries = [normalize_intensities(s) for s in queries if s is not None] queries = [require_minimum_number_of_peaks(s, n_required=5) for s in queries if s is not None] # Load reference library refs = list(load_from_mgf("library.mgf")) refs = [default_filters(s) for s in refs] refs = [normalize_intensities(s) for s in refs if s is not None] # Calculate similarity scores scores = calculate_scores(references=refs, queries=queries, similarity_function=CosineGreedy(tolerance=0.1)) # Get best matches for first query best = scores.scores_by_query(queries[0], sort=True)[:5] for match, score_tuple in best: print(f"Score: {score_tuple['score']:.3f}, Matches: {score_tuple['matches']}")
Import spectra from multiple file formats and export processed data.
pythonfrom matchms.importing import (load_from_mgf, load_from_mzml, load_from_msp, load_from_json, load_from_mzxml, load_from_pickle, load_from_usi) from matchms.exporting import save_as_mgf, save_as_msp, save_as_json, save_as_pickle # Import from various formats (returns generators) spectra_mgf = list(load_from_mgf("library.mgf")) spectra_mzml = list(load_from_mzml("data.mzML")) spectra_msp = list(load_from_msp("nist_library.msp")) spectra_json = list(load_from_json("gnps_spectra.json")) print(f"Loaded: MGF={len(spectra_mgf)}, mzML={len(spectra_mzml)}") # Export processed spectra save_as_mgf(spectra_mgf, "processed.mgf") save_as_json(spectra_mgf, "processed.json") save_as_pickle(spectra_mgf, "spectra.pickle") # Fast for intermediate results # Pickle for large datasets (fastest I/O) from matchms.importing import load_from_pickle spectra = list(load_from_pickle("spectra.pickle"))
pythonfrom matchms import Spectrum import numpy as np # Create spectrum manually mz = np.array([100.0, 150.0, 200.0, 250.0, 300.0]) intensities = np.array([0.1, 0.5, 0.9, 0.3, 0.7]) metadata = { "precursor_mz": 325.5, "ionmode": "positive", "compound_name": "Caffeine", "smiles": "CN1C=NC2=C1C(=O)N(C(=O)N2C)C" } spectrum = Spectrum(mz=mz, intensities=intensities, metadata=metadata) # Access spectrum data print(f"Peaks: {spectrum.peaks.mz}") print(f"Precursor: {spectrum.get('precursor_mz')}") print(f"Name: {spectrum.get('compound_name')}") # Visualize spectrum.plot()
Apply metadata harmonization and peak processing filters. Matchms provides 50+ filters.
pythonfrom matchms.filtering import ( default_filters, normalize_intensities, select_by_relative_intensity, select_by_mz, require_minimum_number_of_peaks, reduce_to_number_of_peaks, remove_peaks_around_precursor_mz, add_losses ) # default_filters applies: metadata cleanup, charge correction, adduct parsing spectrum = default_filters(spectrum) # Peak normalization (max intensity → 1.0) spectrum = normalize_intensities(spectrum) # Filter peaks by relative intensity (remove noise below 1%) spectrum = select_by_relative_intensity(spectrum, intensity_from=0.01, intensity_to=1.0) # Filter peaks by m/z range spectrum = select_by_mz(spectrum, mz_from=50.0, mz_to=500.0) # Keep top N peaks only spectrum = reduce_to_number_of_peaks(spectrum, n_max=50) # Remove peaks near precursor (common contaminants) spectrum = remove_peaks_around_precursor_mz(spectrum, mz_tolerance=17.0) # Require minimum peaks for matching spectrum = require_minimum_number_of_peaks(spectrum, n_required=5) # Add neutral losses (useful for NeutralLossesCosine) spectrum = add_losses(spectrum) if spectrum is not None: print(f"After filtering: {len(spectrum.peaks.mz)} peaks")
python# Chemical annotation filters (require matchms[chemistry]) from matchms.filtering import ( derive_inchi_from_smiles, derive_inchikey_from_inchi, derive_smiles_from_inchi, add_fingerprint, repair_inchi_inchikey_smiles, require_valid_annotation ) # Derive chemical identifiers from SMILES spectrum = derive_inchi_from_smiles(spectrum) spectrum = derive_inchikey_from_inchi(spectrum) # Add molecular fingerprint for structural similarity spectrum = add_fingerprint(spectrum, fingerprint_type="morgan", nbits=2048) # Validate annotations spectrum = require_valid_annotation(spectrum) if spectrum is not None: print(f"InChIKey: {spectrum.get('inchikey')}")
Compare spectra using multiple similarity metrics.
pythonfrom matchms import calculate_scores from matchms.similarity import ( CosineGreedy, CosineHungarian, ModifiedCosine, NeutralLossesCosine, FingerprintSimilarity, MetadataMatch, PrecursorMzMatch ) # CosineGreedy — fast peak matching (greedy algorithm) scores = calculate_scores(references=library, queries=unknowns, similarity_function=CosineGreedy(tolerance=0.1)) # ModifiedCosine — accounts for precursor mass differences (best for analog search) scores = calculate_scores(references=library, queries=unknowns, similarity_function=ModifiedCosine(tolerance=0.1)) # CosineHungarian — optimal peak matching (slower but more accurate) scores = calculate_scores(references=library, queries=unknowns, similarity_function=CosineHungarian(tolerance=0.1)) # NeutralLossesCosine — similarity based on neutral loss patterns scores = calculate_scores(references=library, queries=unknowns, similarity_function=NeutralLossesCosine(tolerance=0.1)) # Access results for i, query in enumerate(unknowns[:3]): best_matches = scores.scores_by_query(query, sort=True)[:3] print(f"\nQuery {i}: precursor_mz={query.get('precursor_mz')}") for ref, score_tuple in best_matches: print(f" {ref.get('compound_name', 'Unknown')}: " f"score={score_tuple['score']:.3f}, matches={score_tuple['matches']}")
python# FingerprintSimilarity — structural similarity (requires fingerprints) from matchms.similarity import FingerprintSimilarity scores = calculate_scores(references=library, queries=unknowns, similarity_function=FingerprintSimilarity( similarity_measure="jaccard")) # PrecursorMzMatch — fast mass-based pre-filtering from matchms.similarity import PrecursorMzMatch scores = calculate_scores(references=library, queries=unknowns, similarity_function=PrecursorMzMatch(tolerance=0.1)) # Multi-metric scoring: combine peak + structural similarity cosine_scores = calculate_scores(references=library, queries=unknowns, similarity_function=CosineGreedy(tolerance=0.1)) fp_scores = calculate_scores(references=library, queries=unknowns, similarity_function=FingerprintSimilarity())
Build reusable, reproducible multi-step processing workflows.
pythonfrom matchms import SpectrumProcessor from matchms.filtering import ( default_filters, normalize_intensities, select_by_relative_intensity, require_minimum_number_of_peaks, remove_peaks_around_precursor_mz, add_losses ) # Define reusable pipeline pipeline = SpectrumProcessor([ default_filters, normalize_intensities, lambda s: select_by_relative_intensity(s, intensity_from=0.01), lambda s: remove_peaks_around_precursor_mz(s, mz_tolerance=17.0), lambda s: require_minimum_number_of_peaks(s, n_required=5), add_losses ]) # Apply to all spectra (filters returning None remove the spectrum) processed = [pipeline(s) for s in raw_spectra] processed = [s for s in processed if s is not None] print(f"Processed: {len(processed)}/{len(raw_spectra)} spectra retained")
| Function | Speed | Accuracy | Best For | |----------|-------|----------|----------| | CosineGreedy | Fast | Good | General library matching | | CosineHungarian | Slow | Best | Small comparisons, validation | | ModifiedCosine | Fast | Good | Analog search (different precursors) | | NeutralLossesCosine | Medium | Good | Structural class identification | | FingerprintSimilarity | Fast | Moderate | Structure-based pre-filtering | | PrecursorMzMatch | Fastest | N/A | Mass-based pre-filtering |
| Category | Examples | Purpose | |----------|----------|---------| | Metadata cleanup | default_filters, clean_compound_name, clean_adduct | Standardize metadata fields | | Chemical derivation | derive_inchi_from_smiles, add_fingerprint | Compute chemical identifiers | | Mass/charge | add_precursor_mz, correct_charge, add_parent_mass | Fix and validate mass info | | Peak normalization | normalize_intensities, select_by_relative_intensity | Scale and filter peaks | | Peak reduction | reduce_to_number_of_peaks, remove_peaks_around_precursor_mz | Remove noise/artifacts | | Quality control | require_minimum_number_of_peaks, require_precursor_mz | Enforce minimum quality | | Neutral losses | add_losses | Compute precursor-fragment losses |
All similarity functions return (score, matches):
score: float 0.0–1.0 (cosine similarity value)matches: int (number of matched peaks between query and reference)Higher scores and more matched peaks indicate better matches. Typical thresholds: score > 0.7 and matches > 6 for confident identifications.
pythonfrom matchms.importing import load_from_mgf from matchms.filtering import default_filters, normalize_intensities from matchms.filtering import select_by_relative_intensity, require_minimum_number_of_peaks from matchms import calculate_scores from matchms.similarity import ModifiedCosine import pandas as pd # Load and process both queries and library identically def process_spectra(spectra): processed = [] for s in spectra: s = default_filters(s) if s is None: continue s = normalize_intensities(s) s = select_by_relative_intensity(s, intensity_from=0.01) s = require_minimum_number_of_peaks(s, n_required=5) if s is not None: processed.append(s) return processed queries = process_spectra(load_from_mgf("unknowns.mgf")) library = process_spectra(load_from_mgf("reference_library.mgf")) print(f"Queries: {len(queries)}, Library: {len(library)}") # Score all query-reference pairs scores = calculate_scores(references=library, queries=queries, similarity_function=ModifiedCosine(tolerance=0.1)) # Extract best matches results = [] for query in queries: best = scores.scores_by_query(query, sort=True)[:1] if best: ref, score_tuple = best[0] results.append({ "query_precursor_mz": query.get("precursor_mz"), "match_name": ref.get("compound_name", "Unknown"), "match_smiles": ref.get("smiles", ""), "score": score_tuple["score"], "matched_peaks": score_tuple["matches"] }) df = pd.DataFrame(results) confident = df[df["score"] > 0.7] print(f"Confident matches (score>0.7): {len(confident)}/{len(df)}") df.to_csv("identification_results.csv", index=False)
pythonfrom matchms.importing import load_from_msp from matchms.exporting import save_as_mgf from matchms import SpectrumProcessor from matchms.filtering import ( default_filters, normalize_intensities, select_by_relative_intensity, require_minimum_number_of_peaks, require_precursor_mz, add_parent_mass ) # Define QC pipeline qc_pipeline = SpectrumProcessor([ default_filters, require_precursor_mz, add_parent_mass, normalize_intensities, lambda s: select_by_relative_intensity(s, intensity_from=0.001), lambda s: require_minimum_number_of_peaks(s, n_required=3) ]) # Process and filter raw = list(load_from_msp("raw_library.msp")) cleaned = [qc_pipeline(s) for s in raw] cleaned = [s for s in cleaned if s is not None] print(f"Input: {len(raw)}, Output: {len(cleaned)} ({len(cleaned)/len(raw)*100:.1f}% retained)") save_as_mgf(cleaned, "cleaned_library.mgf")
load_from_mzml)default_filters for metadata harmonization (Core API Module 2)save_as_mgf)| Parameter | Function/Module | Default | Range/Options | Effect | |-----------|----------------|---------|---------------|--------| | tolerance | CosineGreedy/ModifiedCosine | 0.1 | 0.005–0.5 Da | m/z tolerance for peak matching | | mz_power | CosineGreedy | 0.0 | 0.0–2.0 | Weight of m/z in scoring (0=ignore) | | intensity_power | CosineGreedy | 1.0 | 0.0–2.0 | Weight of intensity in scoring | | intensity_from | select_by_relative_intensity | 0.0 | 0.0–1.0 | Minimum relative intensity to keep | | n_required | require_minimum_number_of_peaks | 10 | 1–100 | Minimum peaks to retain spectrum | | n_max | reduce_to_number_of_peaks | 100 | 10–500 | Maximum peaks to retain | | mz_tolerance | remove_peaks_around_precursor_mz | 17.0 | 0.5–50 Da | Window around precursor to remove | | fingerprint_type | add_fingerprint | "daylight" | "daylight"/"morgan"/"maccs" | Molecular fingerprint type | | nbits | add_fingerprint | 2048 | 256–4096 | Fingerprint bit vector length |
PrecursorMzMatch first to reduce the comparison space, then score with CosineGreedyNone when a spectrum fails quality requirements. Always filter: [s for s in processed if s is not None]pythonfrom matchms import calculate_scores from matchms.similarity import PrecursorMzMatch, CosineGreedy # Step 1: Fast mass filter mass_scores = calculate_scores(references=library, queries=unknowns, similarity_function=PrecursorMzMatch(tolerance=0.5)) # Step 2: Detailed scoring only for mass-matched pairs cosine = CosineGreedy(tolerance=0.1) for query in unknowns: candidates = mass_scores.scores_by_query(query, sort=True) mass_matched = [ref for ref, score in candidates if score["score"] > 0] if mass_matched: detailed = calculate_scores(references=mass_matched, queries=[query], similarity_function=cosine) best = detailed.scores_by_query(query, sort=True)[:3] for ref, s in best: print(f"{ref.get('compound_name')}: {s['score']:.3f}")
pythonfrom matchms.importing import load_from_mgf from matchms.filtering import default_filters, normalize_intensities spectra = list(load_from_mgf("mixed_library.mgf")) spectra = [default_filters(s) for s in spectra] spectra = [s for s in spectra if s is not None] # Separate by ion mode positive = [s for s in spectra if s.get("ionmode") == "positive"] negative = [s for s in spectra if s.get("ionmode") == "negative"] print(f"Positive: {len(positive)}, Negative: {len(negative)}") # Process each mode with mode-specific filtering positive = [normalize_intensities(s) for s in positive] negative = [normalize_intensities(s) for s in negative]
pythonimport pandas as pd from matchms.importing import load_from_mgf from matchms.filtering import default_filters spectra = [default_filters(s) for s in load_from_mgf("library.mgf")] spectra = [s for s in spectra if s is not None] # Extract metadata summary rows = [] for s in spectra: rows.append({ "compound_name": s.get("compound_name", ""), "precursor_mz": s.get("precursor_mz"), "ionmode": s.get("ionmode", ""), "smiles": s.get("smiles", ""), "inchikey": s.get("inchikey", ""), "num_peaks": len(s.peaks.mz) }) df = pd.DataFrame(rows) print(f"Library: {len(df)} spectra") print(f"Named: {(df.compound_name != '').sum()}") print(f"With SMILES: {(df.smiles != '').sum()}") print(f"Ion modes: {df.ionmode.value_counts().to_dict()}")
| Problem | Cause | Solution | |---------|-------|----------| | All scores are 0.0 | No matching peaks within tolerance | Increase tolerance (try 0.2–0.5 Da); verify both spectra have peaks | | Low scores despite same compound | Different fragmentation conditions | Use ModifiedCosine instead of CosineGreedy; check ion mode consistency | | Many spectra filtered to None | Too strict quality filters | Lower n_required in require_minimum_number_of_peaks; relax intensity thresholds | | KeyError on metadata field | Field name not harmonized | Apply default_filters first to harmonize metadata keys | | Memory error with large library | All-vs-all comparison | Pre-filter by precursor mass (PrecursorMzMatch) before detailed scoring | | add_fingerprint fails | RDKit not installed | Install chemistry extras: pip install matchms[chemistry] | | Import returns empty list | Wrong file format or path | Verify format matches loader (MGF for .mgf, MSP for .msp); check file is not empty | | Inconsistent scores between runs | Different processing pipelines | Use SpectrumProcessor to ensure identical processing for queries and references |
spectrum.plot() in Core API Module 1Disposition of original reference files:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 21,294 | 14,539 | -32% | 1 | 1 | 0% | 3,916 | 8,758 | +124% | 0 | 0 | — |
case-02 | pass→pass | 19,990 | 14,605 | -27% | 1 | 1 | 0% | 3,551 | 8,784 | +147% | 0 | 0 | — |
case-03 | fail→fail | 40,373 | 14,299 | -65% | 1 | 1 | 0% | 3,233 | 8,950 | +177% | 0 | 0 | — |
case-04 | pass→pass | 17,542 | 14,580 | -17% | 1 | 1 | 0% | 3,300 | 9,095 | +176% | 0 | 0 | — |
case-05 | pass→pass | 15,289 | 8,768 | -43% | 1 | 1 | 0% | 2,934 | 8,001 | +173% | 0 | 0 | — |
case-06 | pass→pass | 14,660 | 10,050 | -31% | 1 | 1 | 0% | 2,922 | 8,080 | +177% | 0 | 0 | — |
case-07 | pass→pass | 10,317 | 6,522 | -37% | 1 | 1 | 0% | 1,841 | 7,370 | +300% | 0 | 0 | — |
case-08 | fail→pass | 17,711 | 12,795 | -28% | 1 | 1 | 0% | 3,540 | 8,718 | +146% | 0 | 0 | — |
case-09 | pass→pass | 15,954 | 10,718 | -33% | 1 | 1 | 0% | 3,023 | 8,298 | +174% | 0 | 0 | — |
case-10 | fail→pass | 18,173 | 8,107 | -55% | 1 | 1 | 0% | 3,361 | 7,647 | +128% | 0 | 0 | — |
case-11 | pass→pass | 9,531 | 7,889 | -17% | 1 | 1 | 0% | 1,848 | 7,666 | +315% | 0 | 0 | — |
case-12 | fail→pass | 13,129 | 20,029 | +53% | 1 | 1 | 0% | 2,161 | 7,355 | +240% | 0 | 0 | — |
case-13 | fail→pass | 25,601 | 10,035 | -61% | 1 | 1 | 0% | 2,958 | 8,252 | +179% | 0 | 0 | — |
case-14 | fail→pass | 4,672 | 27,004 | +478% | 1 | 1 | 0% | 925 | 6,957 | +652% | 0 | 0 | — |
case-15 | pass→pass | 11,682 | 5,868 | -50% | 1 | 1 | 0% | 2,202 | 7,231 | +228% | 0 | 0 | — |
case-16 | pass→pass | 10,204 | 6,609 | -35% | 1 | 1 | 0% | 2,048 | 7,475 | +265% | 0 | 0 | — |
case-17 | pass→pass | 16,496 | 6,267 | -62% | 1 | 1 | 0% | 1,643 | 7,345 | +347% | 0 | 0 | — |
case-18 | fail→pass | 9,037 | 3,911 | -57% | 1 | 1 | 0% | 1,717 | 6,943 | +304% | 0 | 0 | — |
case-19 | pass→pass | 4,752 | 2,997 | -37% | 1 | 1 | 0% | 1,018 | 6,798 | +568% | 0 | 0 | — |
case-20 | pass→pass | 7,843 | 4,325 | -45% | 1 | 1 | 0% | 1,434 | 6,973 | +386% | 0 | 0 | — |
case-21 | fail→pass | 12,897 | 4,574 | -65% | 1 | 1 | 0% | 2,693 | 7,078 | +163% | 0 | 0 | — |
case-22 | pass→pass | 8,860 | 9,024 | +2% | 1 | 1 | 0% | 1,775 | 7,935 | +347% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.