ultrams 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. ultrams-0.3.0/CITATION.cff +15 -0
  2. ultrams-0.3.0/MANIFEST.in +4 -0
  3. {ultrams-0.2.0/src/ultrams.egg-info → ultrams-0.3.0}/PKG-INFO +11 -2
  4. {ultrams-0.2.0 → ultrams-0.3.0}/README.md +10 -1
  5. ultrams-0.3.0/cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md +22 -0
  6. ultrams-0.3.0/cookbook/tutorials/README.md +13 -0
  7. ultrams-0.3.0/cookbook/tutorials/embed.ipynb +83 -0
  8. ultrams-0.3.0/cookbook/tutorials/massspecgym_contrastive.py +301 -0
  9. ultrams-0.3.0/cookbook/tutorials/pytorch_finetune.ipynb +97 -0
  10. ultrams-0.3.0/cookbook/tutorials/spectrum_search.ipynb +69 -0
  11. {ultrams-0.2.0 → ultrams-0.3.0}/docs/data-format.md +15 -0
  12. {ultrams-0.2.0 → ultrams-0.3.0}/examples/README.md +13 -0
  13. {ultrams-0.2.0 → ultrams-0.3.0}/examples/pytorch_finetune.py +3 -0
  14. {ultrams-0.2.0 → ultrams-0.3.0}/pyproject.toml +1 -1
  15. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/inference.py +18 -1
  16. {ultrams-0.2.0 → ultrams-0.3.0/src/ultrams.egg-info}/PKG-INFO +11 -2
  17. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/SOURCES.txt +7 -0
  18. {ultrams-0.2.0 → ultrams-0.3.0}/tests/test_model_api.py +15 -0
  19. ultrams-0.2.0/MANIFEST.in +0 -2
  20. {ultrams-0.2.0 → ultrams-0.3.0}/LICENSE +0 -0
  21. {ultrams-0.2.0 → ultrams-0.3.0}/docs/model-selection.md +0 -0
  22. {ultrams-0.2.0 → ultrams-0.3.0}/examples/batch_embeddings.py +0 -0
  23. {ultrams-0.2.0 → ultrams-0.3.0}/examples/data/NOTICE.md +0 -0
  24. {ultrams-0.2.0 → ultrams-0.3.0}/examples/data/example_5_spectra.mgf +0 -0
  25. {ultrams-0.2.0 → ultrams-0.3.0}/examples/spectrum_search.py +0 -0
  26. {ultrams-0.2.0 → ultrams-0.3.0}/setup.cfg +0 -0
  27. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/__init__.py +0 -0
  28. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/_architecture.py +0 -0
  29. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/cli.py +0 -0
  30. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/finetune.py +0 -0
  31. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams/spectra.py +0 -0
  32. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/dependency_links.txt +0 -0
  33. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/entry_points.txt +0 -0
  34. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/requires.txt +0 -0
  35. {ultrams-0.2.0 → ultrams-0.3.0}/src/ultrams.egg-info/top_level.txt +0 -0
  36. {ultrams-0.2.0 → ultrams-0.3.0}/tests/test_spectra.py +0 -0
@@ -0,0 +1,15 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use UltraMS in your research, please cite this software."
3
+ type: software
4
+ title: UltraMS
5
+ authors:
6
+ - family-names: Li
7
+ given-names: Yifan
8
+ - family-names: Zhong
9
+ given-names: Yunhua
10
+ - family-names: Liu
11
+ given-names: Pan
12
+ - family-names: Xia
13
+ given-names: Jun
14
+ repository-code: https://github.com/Dsadd4/UltraMS
15
+ license: Apache-2.0
@@ -0,0 +1,4 @@
1
+ recursive-include docs *.md
2
+ recursive-include examples *.py *.md *.mgf
3
+ recursive-include cookbook/tutorials *.ipynb *.md *.py
4
+ include CITATION.cff
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ultrams
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Pretrained UltraMS models for mass spectrometry
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/Dsadd4/UltraMS
@@ -30,7 +30,8 @@ python -m pip install ultrams
30
30
  | Embed an MGF or mzML file | [File command](#embed-a-spectrum-file) |
31
31
  | Train on my labelled spectra | [PyTorch example](#fine-tune-with-pytorch) · [complete MGF example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) |
32
32
  | Compare spectra | [Search example](https://github.com/Dsadd4/UltraMS/blob/main/examples/spectrum_search.py) |
33
- | Reproduce pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
33
+ | Learn in a notebook | [UltraMS tutorials](https://github.com/Dsadd4/UltraMS/tree/main/cookbook/tutorials) |
34
+ | Inspect or run pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
34
35
 
35
36
  ## Embed a spectrum
36
37
 
@@ -48,6 +49,8 @@ print(embedding.shape) # (1024,)
48
49
 
49
50
  The first call downloads the selected checkpoint from Hugging Face. Use `device="cuda"` or `device="mps"` with `from_pretrained` to run on an available accelerator.
50
51
 
52
+ The three checkpoints are collected on [Hugging Face](https://huggingface.co/collections/dsadd4/ultrams-6ab4cfaa860cbbd1f9a6b9b6).
53
+
51
54
  ## Embed a spectrum file
52
55
 
53
56
  ```bash
@@ -87,6 +90,8 @@ for batch in loader:
87
90
 
88
91
  The [complete fine-tuning example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) reads an MGF file and saves training history. For a shorter route, use `model.finetune(labelled_spectra, task="regression")` with a `label` field; it saves weights, configuration, and losses. See [spectrum input](https://github.com/Dsadd4/UltraMS/blob/main/docs/data-format.md) for the input format.
89
92
 
93
+ For measured molecular labels, the [MassSpecGym contrastive tutorial](https://github.com/Dsadd4/UltraMS/blob/main/cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md) downloads the dataset, keeps its train/validation/test folds, fine-tunes the encoder, and reloads the selected checkpoint.
94
+
90
95
  ## Choose a model
91
96
 
92
97
  | Model name | Representation | Dimension | Spectral peak limit | Weights |
@@ -100,3 +105,7 @@ Load any of them with `UltraMS.from_pretrained("name")`, or load a downloaded `m
100
105
  ## Pretraining
101
106
 
102
107
  The [UltraMSdata pretraining package](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) contains the current model architecture, training entry points, configuration and history outputs. It installs separately from the inference package.
108
+
109
+ ## Cite
110
+
111
+ Citation metadata for this software is in [CITATION.cff](https://github.com/Dsadd4/UltraMS/blob/main/CITATION.cff).
@@ -12,7 +12,8 @@ python -m pip install ultrams
12
12
  | Embed an MGF or mzML file | [File command](#embed-a-spectrum-file) |
13
13
  | Train on my labelled spectra | [PyTorch example](#fine-tune-with-pytorch) · [complete MGF example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) |
14
14
  | Compare spectra | [Search example](https://github.com/Dsadd4/UltraMS/blob/main/examples/spectrum_search.py) |
15
- | Reproduce pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
15
+ | Learn in a notebook | [UltraMS tutorials](https://github.com/Dsadd4/UltraMS/tree/main/cookbook/tutorials) |
16
+ | Inspect or run pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
16
17
 
17
18
  ## Embed a spectrum
18
19
 
@@ -30,6 +31,8 @@ print(embedding.shape) # (1024,)
30
31
 
31
32
  The first call downloads the selected checkpoint from Hugging Face. Use `device="cuda"` or `device="mps"` with `from_pretrained` to run on an available accelerator.
32
33
 
34
+ The three checkpoints are collected on [Hugging Face](https://huggingface.co/collections/dsadd4/ultrams-6ab4cfaa860cbbd1f9a6b9b6).
35
+
33
36
  ## Embed a spectrum file
34
37
 
35
38
  ```bash
@@ -69,6 +72,8 @@ for batch in loader:
69
72
 
70
73
  The [complete fine-tuning example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) reads an MGF file and saves training history. For a shorter route, use `model.finetune(labelled_spectra, task="regression")` with a `label` field; it saves weights, configuration, and losses. See [spectrum input](https://github.com/Dsadd4/UltraMS/blob/main/docs/data-format.md) for the input format.
71
74
 
75
+ For measured molecular labels, the [MassSpecGym contrastive tutorial](https://github.com/Dsadd4/UltraMS/blob/main/cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md) downloads the dataset, keeps its train/validation/test folds, fine-tunes the encoder, and reloads the selected checkpoint.
76
+
72
77
  ## Choose a model
73
78
 
74
79
  | Model name | Representation | Dimension | Spectral peak limit | Weights |
@@ -82,3 +87,7 @@ Load any of them with `UltraMS.from_pretrained("name")`, or load a downloaded `m
82
87
  ## Pretraining
83
88
 
84
89
  The [UltraMSdata pretraining package](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) contains the current model architecture, training entry points, configuration and history outputs. It installs separately from the inference package.
90
+
91
+ ## Cite
92
+
93
+ Citation metadata for this software is in [CITATION.cff](https://github.com/Dsadd4/UltraMS/blob/main/CITATION.cff).
@@ -0,0 +1,22 @@
1
+ # Contrastive fine-tuning with MassSpecGym
2
+
3
+ This tutorial fine-tunes the **UltraMS Unsupervised** encoder on MassSpecGym. Two distinct MS/MS spectra with the same `inchikey` are a positive pair; spectra from different molecules in a batch are negatives. The dataset's `train`, `val`, and `test` folds are retained, and the script checks that no molecular key appears in more than one fold. It keeps spectra with at least three positive-intensity, positive-m/z peaks and records the source and selected row and molecule counts in `data_audit.json`.
4
+
5
+ Install UltraMS, then run the small teaching subset from the repository root:
6
+
7
+ ```bash
8
+ python -m pip install -e .
9
+ python cookbook/tutorials/massspecgym_contrastive.py --output-dir /path/to/my_run
10
+ ```
11
+
12
+ The default selects up to 64 train molecules and 24 molecules from each evaluation fold, with two distinct spectra per molecule. Selection is deterministic. To use every training molecule with at least two distinct spectra and all valid evaluation spectra, after removing exact spectral duplicates:
13
+
14
+ ```bash
15
+ python cookbook/tutorials/massspecgym_contrastive.py --full --output-dir /path/to/full_run
16
+ ```
17
+
18
+ The full run can take substantially longer. `--data-file /path/to/MassSpecGym.tsv` reuses a local copy; otherwise the script downloads the pinned file through Hugging Face Hub. In both cases it checks SHA-256 before reading. The source is [`roman-bushuiev/MassSpecGym`](https://huggingface.co/datasets/roman-bushuiev/MassSpecGym), revision `d2e86d0c3bd905a6d578c0dd6053ed2bd41f9c2a`, file `data/MassSpecGym.tsv`, SHA-256 `0c9cc50450def3f0d4fe2dc09dea1105fc15e635db8c6656bc3e3be37a3bcd95`.
19
+
20
+ The script writes `best.pt`, `config.json`, `data_audit.json`, `history.json`, and `test.json`. Validation selects the checkpoint; the held-out test fold is evaluated once afterward. Top1 means nearest *other spectrum* within the same fold has the same `inchikey`; singleton molecules are candidates but cannot be queries. This is a **tutorial retrieval task**, not an official MassSpecGym benchmark or a reported UltraMS result. The label is molecular identity from the dataset, not measured retention time or logP.
21
+
22
+ For the original dataset and citation, see [MassSpecGym](https://github.com/pluskal-lab/MassSpecGym).
@@ -0,0 +1,13 @@
1
+ # UltraMS tutorials
2
+
3
+ Each notebook runs from top to bottom on the [five-spectrum MGF example](../../examples/data/NOTICE.md). It installs UltraMS from PyPI and downloads the selected model from Hugging Face on first use.
4
+
5
+ | Tutorial | Notebook | Colab |
6
+ | --- | --- | --- |
7
+ | Read MGF and obtain embeddings | [Get embeddings](embed.ipynb) | [Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/embed.ipynb) |
8
+ | Fine-tune in a PyTorch loop | [PyTorch fine-tuning](pytorch_finetune.ipynb) | [Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/pytorch_finetune.ipynb) |
9
+ | Rank spectra with the Search model | [Spectrum search](spectrum_search.ipynb) | [Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/spectrum_search.ipynb) |
10
+
11
+ The fine-tuning labels are placeholders for learning the API. They do not represent measured properties or a model performance result.
12
+
13
+ For a complete fine-tuning example with real molecular labels and the original dataset folds, follow the [MassSpecGym contrastive tutorial](MASSSPECGYM_CONTRASTIVE.md).
@@ -0,0 +1,83 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "introduction",
6
+ "metadata": {},
7
+ "source": "# Get MS/MS embeddings\n\n[Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/embed.ipynb)\n\nRead a real MGF file and obtain one UltraMS embedding per spectrum. The input file is the [five-spectrum DreaMS example](../../examples/data/NOTICE.md). Run cells from top to bottom."
8
+ },
9
+ {
10
+ "cell_type": "markdown",
11
+ "id": "installation",
12
+ "metadata": {},
13
+ "source": "## Install"
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "id": "install-ultrams",
18
+ "metadata": {},
19
+ "execution_count": null,
20
+ "outputs": [],
21
+ "source": [
22
+ "%pip install -q ultrams\n"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "markdown",
27
+ "id": "read-spectra",
28
+ "metadata": {},
29
+ "source": "## Read spectra"
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "id": "load-mgf",
34
+ "metadata": {},
35
+ "execution_count": null,
36
+ "outputs": [],
37
+ "source": "from pathlib import Path\nfrom urllib.request import urlretrieve\nimport torch\nfrom ultrams import UltraMS, read_spectra\n\nsample = Path(\"examples/data/example_5_spectra.mgf\")\nif not sample.exists():\n sample = Path(\"example_5_spectra.mgf\")\n if not sample.exists():\n urlretrieve(\n \"https://raw.githubusercontent.com/Dsadd4/UltraMS/main/examples/data/example_5_spectra.mgf\",\n sample,\n )\nspectra = list(read_spectra(sample))\nassert len(spectra) == 5\nprint(f\"Loaded {len(spectra)} MS/MS spectra\")"
38
+ },
39
+ {
40
+ "cell_type": "markdown",
41
+ "id": "encode-heading",
42
+ "metadata": {},
43
+ "source": "## Encode in batches"
44
+ },
45
+ {
46
+ "cell_type": "code",
47
+ "id": "encode-batch",
48
+ "metadata": {},
49
+ "execution_count": null,
50
+ "outputs": [],
51
+ "source": "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel = UltraMS.from_pretrained(\"unsupervised\", device=device)\nembeddings = model.encode_batch(spectra, batch_size=5)\nassert embeddings.shape == (len(spectra), model.embedding_dim)\nprint(\"Embedding matrix:\", embeddings.shape)\nprint(\"First spectrum:\", spectra[0][\"id\"])"
52
+ },
53
+ {
54
+ "cell_type": "markdown",
55
+ "id": "save-heading",
56
+ "metadata": {},
57
+ "source": "## Save embeddings"
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "id": "save-embeddings",
62
+ "metadata": {},
63
+ "execution_count": null,
64
+ "outputs": [],
65
+ "source": "import numpy as np\n\noutput = Path(\"ultrams_embeddings.npz\")\nnp.savez_compressed(output, ids=[spectrum[\"id\"] for spectrum in spectra], embeddings=embeddings)\nprint(output.resolve())"
66
+ }
67
+ ],
68
+ "metadata": {
69
+ "kernelspec": {
70
+ "display_name": "Python 3",
71
+ "language": "python",
72
+ "name": "python3"
73
+ },
74
+ "language_info": {
75
+ "name": "python"
76
+ },
77
+ "colab": {
78
+ "provenance": []
79
+ }
80
+ },
81
+ "nbformat": 4,
82
+ "nbformat_minor": 5
83
+ }
@@ -0,0 +1,301 @@
1
+ """Fine-tune UltraMS on MassSpecGym spectra sharing a molecular InChIKey."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import csv
7
+ import hashlib
8
+ import json
9
+ import random
10
+ import tempfile
11
+ from collections import Counter, defaultdict
12
+ from pathlib import Path
13
+
14
+ import numpy as np
15
+ import torch
16
+ import torch.nn.functional as F
17
+ from huggingface_hub import hf_hub_download
18
+
19
+ from ultrams import UltraMS
20
+
21
+
22
+ DATASET = "roman-bushuiev/MassSpecGym"
23
+ REVISION = "d2e86d0c3bd905a6d578c0dd6053ed2bd41f9c2a"
24
+ FILENAME = "data/MassSpecGym.tsv"
25
+ SHA256 = "0c9cc50450def3f0d4fe2dc09dea1105fc15e635db8c6656bc3e3be37a3bcd95"
26
+ FOLDS = ("train", "val", "test")
27
+
28
+
29
+ def checked_data_file(path: Path | None) -> Path:
30
+ if path is None:
31
+ path = Path(hf_hub_download(repo_id=DATASET, repo_type="dataset", filename=FILENAME, revision=REVISION))
32
+ digest = hashlib.sha256()
33
+ with path.open("rb") as handle:
34
+ for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
35
+ digest.update(chunk)
36
+ if digest.hexdigest() != SHA256:
37
+ raise ValueError(f"MassSpecGym SHA-256 mismatch for {path}")
38
+ return path
39
+
40
+
41
+ def rows_in(path: Path):
42
+ with path.open(newline="", encoding="utf-8") as handle:
43
+ reader = csv.DictReader(handle, delimiter="\t")
44
+ needed = {"identifier", "mzs", "intensities", "inchikey", "precursor_mz", "fold"}
45
+ missing = needed - set(reader.fieldnames or [])
46
+ if missing:
47
+ raise ValueError(f"MassSpecGym is missing columns: {sorted(missing)}")
48
+ yield from reader
49
+
50
+
51
+ def parse_spectrum(row: dict[str, str]):
52
+ key = row["inchikey"].strip()
53
+ if not key:
54
+ return None
55
+ try:
56
+ mz = np.fromstring(row["mzs"], sep=",", dtype=np.float32)
57
+ intensity = np.fromstring(row["intensities"], sep=",", dtype=np.float32)
58
+ precursor = float(row["precursor_mz"])
59
+ except ValueError:
60
+ return None
61
+ if mz.size != intensity.size or not np.isfinite(precursor) or precursor <= 0:
62
+ return None
63
+ valid = np.isfinite(mz) & np.isfinite(intensity) & (mz > 0)
64
+ effective = valid & (intensity > 0)
65
+ if int(effective.sum()) < 3:
66
+ return None
67
+ # Keep zero-intensity peaks, as UltraMS inference does.
68
+ mz, intensity = mz[valid], intensity[valid]
69
+ fingerprint = hashlib.blake2b(
70
+ mz.tobytes() + intensity.tobytes() + np.float32(precursor).tobytes(), digest_size=16
71
+ ).hexdigest()
72
+ return {
73
+ "id": row["identifier"],
74
+ "inchikey": key,
75
+ "mz": mz,
76
+ "intensity": intensity,
77
+ "precursor_mz": precursor,
78
+ "fingerprint": fingerprint,
79
+ }
80
+
81
+
82
+ def selected_keys(groups: dict[str, set[str]], fold: str, seed: int, limit: int | None):
83
+ keys = sorted(groups, key=lambda key: hashlib.sha256(f"{seed}:{fold}:{key}".encode()).digest())
84
+ return set(keys if limit is None else keys[:limit])
85
+
86
+
87
+ def prepare(path: Path, *, seed: int, full: bool, limits: dict[str, int]):
88
+ raw = Counter()
89
+ valid = Counter()
90
+ fingerprints = {fold: defaultdict(set) for fold in FOLDS}
91
+ for row in rows_in(path):
92
+ fold = row["fold"]
93
+ if fold not in FOLDS:
94
+ raise ValueError(f"unexpected MassSpecGym fold: {fold!r}")
95
+ raw[fold] += 1
96
+ spectrum = parse_spectrum(row)
97
+ if spectrum is None:
98
+ continue
99
+ valid[fold] += 1
100
+ fingerprints[fold][spectrum["inchikey"]].add(spectrum["fingerprint"])
101
+
102
+ keys = {fold: set(fingerprints[fold]) for fold in FOLDS}
103
+ for i, left in enumerate(FOLDS):
104
+ for right in FOLDS[i + 1 :]:
105
+ if keys[left] & keys[right]:
106
+ raise ValueError(f"molecular overlap between {left} and {right}")
107
+
108
+ eligible = {
109
+ fold: {key: values for key, values in fingerprints[fold].items() if len(values) >= 2}
110
+ for fold in FOLDS
111
+ }
112
+ chosen = {
113
+ "train": selected_keys(eligible["train"], "train", seed, None if full else limits["train"]),
114
+ "val": selected_keys(fingerprints["val"] if full else eligible["val"], "val", seed, None if full else limits["val"]),
115
+ "test": selected_keys(fingerprints["test"] if full else eligible["test"], "test", seed, None if full else limits["test"]),
116
+ }
117
+ if any(not chosen[fold] for fold in FOLDS):
118
+ raise ValueError("at least one fold has no selected molecules with valid spectra")
119
+
120
+ spectra = {fold: defaultdict(list) for fold in FOLDS}
121
+ seen = {fold: defaultdict(set) for fold in FOLDS}
122
+ duplicates = Counter()
123
+ for row in rows_in(path):
124
+ fold, key = row["fold"], row["inchikey"].strip()
125
+ if key not in chosen[fold]:
126
+ continue
127
+ spectrum = parse_spectrum(row)
128
+ if spectrum is None:
129
+ continue
130
+ fingerprint = spectrum.pop("fingerprint")
131
+ if fingerprint in seen[fold][key]:
132
+ duplicates[fold] += 1
133
+ continue
134
+ seen[fold][key].add(fingerprint)
135
+ if not full and len(spectra[fold][key]) >= 2:
136
+ continue
137
+ spectra[fold][key].append(spectrum)
138
+
139
+ for fold in FOLDS:
140
+ if fold == "train" or not full:
141
+ if any(len(group) < 2 for group in spectra[fold].values()):
142
+ raise ValueError(f"{fold} contains a selected molecule without two distinct spectra")
143
+
144
+ audit = {
145
+ "source": {"dataset": DATASET, "revision": REVISION, "file": FILENAME, "sha256": SHA256},
146
+ "mode": "full" if full else "teaching subset",
147
+ "selection_seed": seed,
148
+ "folds": {
149
+ fold: {
150
+ "source_rows": raw[fold],
151
+ "valid_rows_at_least_three_peaks": valid[fold],
152
+ "source_molecules_after_filter": len(fingerprints[fold]),
153
+ "molecules_with_two_distinct_spectra": len(eligible[fold]),
154
+ "selected_rows": sum(map(len, spectra[fold].values())),
155
+ "selected_molecules": len(spectra[fold]),
156
+ "selected_molecules_with_two_spectra": sum(len(group) >= 2 for group in spectra[fold].values()),
157
+ "retrieval_queries_with_positive": sum(
158
+ len(group) for group in spectra[fold].values() if len(group) >= 2
159
+ ),
160
+ "exact_duplicate_spectra_removed": duplicates[fold],
161
+ }
162
+ for fold in FOLDS
163
+ },
164
+ "molecule_overlap_between_folds": 0,
165
+ }
166
+ return spectra, audit
167
+
168
+
169
+ def pairs_for_epoch(groups, seed: int, epoch: int):
170
+ rng = random.Random(seed + epoch)
171
+ keys = list(groups)
172
+ rng.shuffle(keys)
173
+ return [rng.sample(groups[key], 2) for key in keys]
174
+
175
+
176
+ def pair_batches(pairs, batch_size: int):
177
+ batches = [pairs[start : start + batch_size] for start in range(0, len(pairs), batch_size)]
178
+ if len(batches) > 1 and len(batches[-1]) == 1:
179
+ batches[-2].extend(batches.pop())
180
+ return batches
181
+
182
+
183
+ def top1(model: UltraMS, groups, *, batch_size: int, device: torch.device):
184
+ records = [record for molecule in groups.values() for record in molecule]
185
+ labels = [record["inchikey"] for record in records]
186
+ counts = Counter(labels)
187
+ embeddings = model.encode_batch(records, batch_size=batch_size)
188
+ vectors = F.normalize(torch.from_numpy(embeddings).to(device), dim=-1)
189
+ correct = queries = 0
190
+ for start in range(0, len(records), 256):
191
+ end = min(start + 256, len(records))
192
+ scores = vectors[start:end] @ vectors.T
193
+ scores[torch.arange(end - start, device=device), torch.arange(start, end, device=device)] = -float("inf")
194
+ nearest = scores.argmax(dim=1).cpu().tolist()
195
+ for offset, neighbour in enumerate(nearest):
196
+ index = start + offset
197
+ if counts[labels[index]] >= 2:
198
+ queries += 1
199
+ correct += labels[index] == labels[neighbour]
200
+ return {"top1": correct / queries, "correct": correct, "queries": queries, "candidates": len(records)}
201
+
202
+
203
+ def write_json(path: Path, content) -> None:
204
+ path.write_text(json.dumps(content, indent=2) + "\n")
205
+
206
+
207
+ def main() -> None:
208
+ parser = argparse.ArgumentParser(description=__doc__)
209
+ parser.add_argument("--data-file", type=Path, help="Use an already downloaded copy of the pinned TSV")
210
+ parser.add_argument("--output-dir", type=Path, help="Keep run files here; otherwise use a temporary directory")
211
+ parser.add_argument("--full", action="store_true", help="Use all valid spectra, not the teaching subset")
212
+ parser.add_argument("--train-molecules", type=int, default=64)
213
+ parser.add_argument("--val-molecules", type=int, default=24)
214
+ parser.add_argument("--test-molecules", type=int, default=24)
215
+ parser.add_argument("--epochs", type=int, default=2)
216
+ parser.add_argument("--batch-size", type=int, default=8, help="Number of positive pairs per training batch")
217
+ parser.add_argument("--lr", type=float, default=1e-5)
218
+ parser.add_argument("--temperature", type=float, default=0.07)
219
+ parser.add_argument("--seed", type=int, default=42)
220
+ parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
221
+ parser.add_argument("--prepare-only", action="store_true", help="Audit and select data without loading the model")
222
+ args = parser.parse_args()
223
+ if any(value < 1 for value in (args.train_molecules, args.val_molecules, args.test_molecules, args.epochs)) or args.batch_size < 2:
224
+ parser.error("molecule limits and epochs must be positive; batch size needs at least two positive pairs")
225
+ if args.lr <= 0 or args.temperature <= 0:
226
+ parser.error("learning rate and temperature must be positive")
227
+
228
+ output = args.output_dir or Path(tempfile.mkdtemp(prefix="ultrams-massspecgym-"))
229
+ output.mkdir(parents=True, exist_ok=True)
230
+ path = checked_data_file(args.data_file)
231
+ groups, audit = prepare(
232
+ path,
233
+ seed=args.seed,
234
+ full=args.full,
235
+ limits={"train": args.train_molecules, "val": args.val_molecules, "test": args.test_molecules},
236
+ )
237
+ if len(groups["train"]) < 2:
238
+ raise ValueError("contrastive training needs at least two distinct training molecules")
239
+ write_json(output / "data_audit.json", audit)
240
+ config = {
241
+ "pretrained_model": "unsupervised",
242
+ "objective": "in-batch contrastive learning; positives are two distinct spectra with the same MassSpecGym inchikey",
243
+ "evaluation": "same-fold spectrum retrieval Top1, self-match excluded; singleton queries excluded",
244
+ "dataset": audit["source"],
245
+ "mode": audit["mode"],
246
+ "seed": args.seed,
247
+ "epochs": args.epochs,
248
+ "batch_size_in_positive_pairs": args.batch_size,
249
+ "learning_rate": args.lr,
250
+ "temperature": args.temperature,
251
+ "device": args.device,
252
+ "output_dir": str(output.resolve()),
253
+ }
254
+ write_json(output / "config.json", config)
255
+ print(json.dumps(audit["folds"], indent=2), flush=True)
256
+ print(f"Run directory: {output.resolve()}", flush=True)
257
+ if args.prepare_only:
258
+ return
259
+
260
+ random.seed(args.seed)
261
+ np.random.seed(args.seed)
262
+ torch.manual_seed(args.seed)
263
+ device = torch.device(args.device)
264
+ model = UltraMS.from_pretrained("unsupervised", device=device)
265
+ optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr)
266
+ history = []
267
+ best_top1 = -1.0
268
+ for epoch in range(1, args.epochs + 1):
269
+ model.train()
270
+ pairs = pairs_for_epoch(groups["train"], args.seed, epoch)
271
+ sum_loss = 0.0
272
+ for mini in pair_batches(pairs, args.batch_size):
273
+ batch = model.collate([spectrum for pair in mini for spectrum in pair])
274
+ embeddings = model(
275
+ batch["peaks"].to(device), batch["attention_mask"].to(device), batch["precursor_mz"].to(device)
276
+ )
277
+ logits = embeddings @ embeddings.T / args.temperature
278
+ logits = logits.masked_fill(torch.eye(len(embeddings), device=device, dtype=torch.bool), -float("inf"))
279
+ target = torch.arange(len(embeddings), device=device) ^ 1
280
+ loss = F.cross_entropy(logits, target)
281
+ optimizer.zero_grad()
282
+ loss.backward()
283
+ optimizer.step()
284
+ sum_loss += float(loss.detach()) * len(mini)
285
+ validation = top1(model, groups["val"], batch_size=args.batch_size * 2, device=device)
286
+ result = {"epoch": epoch, "train_loss": sum_loss / len(pairs), "validation": validation}
287
+ history.append(result)
288
+ write_json(output / "history.json", history)
289
+ print(json.dumps(result), flush=True)
290
+ if validation["top1"] > best_top1:
291
+ best_top1 = validation["top1"]
292
+ torch.save({"model_state_dict": model.model.state_dict(), "config": model.config}, output / "best.pt")
293
+
294
+ best = UltraMS.from_checkpoint(output / "best.pt", device=device)
295
+ test = top1(best, groups["test"], batch_size=args.batch_size * 2, device=device)
296
+ write_json(output / "test.json", test)
297
+ print("Test on the best validation checkpoint:", json.dumps(test), flush=True)
298
+
299
+
300
+ if __name__ == "__main__":
301
+ main()
@@ -0,0 +1,97 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "introduction",
6
+ "metadata": {},
7
+ "source": "# Fine-tune with PyTorch\n\n[Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/pytorch_finetune.ipynb)\n\nUse UltraMS as a standard `torch.nn.Module` in a training loop. The input file is the [five-spectrum DreaMS example](../../examples/data/NOTICE.md). Run cells from top to bottom."
8
+ },
9
+ {
10
+ "cell_type": "markdown",
11
+ "id": "installation",
12
+ "metadata": {},
13
+ "source": "## Install"
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "id": "install-ultrams",
18
+ "metadata": {},
19
+ "execution_count": null,
20
+ "outputs": [],
21
+ "source": [
22
+ "%pip install -q ultrams\n"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "markdown",
27
+ "id": "read-spectra",
28
+ "metadata": {},
29
+ "source": "## Read spectra"
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "id": "load-mgf",
34
+ "metadata": {},
35
+ "execution_count": null,
36
+ "outputs": [],
37
+ "source": "from pathlib import Path\nfrom urllib.request import urlretrieve\nimport torch\nfrom ultrams import UltraMS, read_spectra\n\nsample = Path(\"examples/data/example_5_spectra.mgf\")\nif not sample.exists():\n sample = Path(\"example_5_spectra.mgf\")\n if not sample.exists():\n urlretrieve(\n \"https://raw.githubusercontent.com/Dsadd4/UltraMS/main/examples/data/example_5_spectra.mgf\",\n sample,\n )\nspectra = list(read_spectra(sample))\nassert len(spectra) == 5\nprint(f\"Loaded {len(spectra)} MS/MS spectra\")"
38
+ },
39
+ {
40
+ "cell_type": "markdown",
41
+ "id": "labels-heading",
42
+ "metadata": {},
43
+ "source": "## Add demonstration labels\n\nThe five targets below are **placeholders made from file order**. They are not measured properties or research labels; replace them with your own labels for a scientific task."
44
+ },
45
+ {
46
+ "cell_type": "code",
47
+ "id": "demo-labels",
48
+ "metadata": {},
49
+ "execution_count": null,
50
+ "outputs": [],
51
+ "source": "from torch.utils.data import DataLoader\n\nrecords = [\n {**spectrum, \"target\": index / (len(spectra) - 1)}\n for index, spectrum in enumerate(spectra)\n]\nprint([record[\"target\"] for record in records])"
52
+ },
53
+ {
54
+ "cell_type": "markdown",
55
+ "id": "train-heading",
56
+ "metadata": {},
57
+ "source": "## Train encoder and task head"
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "id": "native-training",
62
+ "metadata": {},
63
+ "execution_count": null,
64
+ "outputs": [],
65
+ "source": "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel = UltraMS.from_pretrained(\"unsupervised\", device=device).train()\nhead = torch.nn.Linear(model.embedding_dim, 1).to(device)\nloader = DataLoader(records, batch_size=2, collate_fn=model.batch_converter())\noptimizer = torch.optim.AdamW([*model.parameters(), *head.parameters()], lr=1e-5)\n\nlosses = []\nfor batch in loader:\n batch = {name: value.to(device) for name, value in batch.items()}\n prediction = head(model(batch[\"peaks\"], batch[\"attention_mask\"], batch[\"precursor_mz\"]))\n loss = torch.nn.functional.mse_loss(prediction.squeeze(-1), batch[\"target\"])\n optimizer.zero_grad()\n loss.backward()\n optimizer.step()\n losses.append(float(loss.detach()))\nprint(\"Batch losses (demonstration only):\", losses)"
66
+ },
67
+ {
68
+ "cell_type": "markdown",
69
+ "id": "save-heading",
70
+ "metadata": {},
71
+ "source": "## Save the run configuration and history\n\nThe weights trained on placeholder labels are only a code demonstration."
72
+ },
73
+ {
74
+ "cell_type": "code",
75
+ "id": "save-history",
76
+ "metadata": {},
77
+ "execution_count": null,
78
+ "outputs": [],
79
+ "source": "import json\n\noutput = Path(\"ultrams_finetune_demo.json\")\noutput.write_text(json.dumps({\n \"model\": \"unsupervised\",\n \"target\": \"placeholder labels from file order\",\n \"batch_size\": 2,\n \"learning_rate\": 1e-5,\n \"loss_per_batch\": losses,\n}, indent=2) + \"\\n\")\nprint(output.resolve())"
80
+ }
81
+ ],
82
+ "metadata": {
83
+ "kernelspec": {
84
+ "display_name": "Python 3",
85
+ "language": "python",
86
+ "name": "python3"
87
+ },
88
+ "language_info": {
89
+ "name": "python"
90
+ },
91
+ "colab": {
92
+ "provenance": []
93
+ }
94
+ },
95
+ "nbformat": 4,
96
+ "nbformat_minor": 5
97
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "introduction",
6
+ "metadata": {},
7
+ "source": "# Search similar MS/MS spectra\n\n[Open in Colab](https://colab.research.google.com/github/Dsadd4/UltraMS/blob/main/cookbook/tutorials/spectrum_search.ipynb)\n\nRank the other spectra in the same example file by cosine similarity using the UltraMS Search model. The input file is the [five-spectrum DreaMS example](../../examples/data/NOTICE.md). Run cells from top to bottom."
8
+ },
9
+ {
10
+ "cell_type": "markdown",
11
+ "id": "installation",
12
+ "metadata": {},
13
+ "source": "## Install"
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "id": "install-ultrams",
18
+ "metadata": {},
19
+ "execution_count": null,
20
+ "outputs": [],
21
+ "source": [
22
+ "%pip install -q ultrams\n"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "markdown",
27
+ "id": "read-spectra",
28
+ "metadata": {},
29
+ "source": "## Read spectra"
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "id": "load-mgf",
34
+ "metadata": {},
35
+ "execution_count": null,
36
+ "outputs": [],
37
+ "source": "from pathlib import Path\nfrom urllib.request import urlretrieve\nimport torch\nfrom ultrams import UltraMS, read_spectra\n\nsample = Path(\"examples/data/example_5_spectra.mgf\")\nif not sample.exists():\n sample = Path(\"example_5_spectra.mgf\")\n if not sample.exists():\n urlretrieve(\n \"https://raw.githubusercontent.com/Dsadd4/UltraMS/main/examples/data/example_5_spectra.mgf\",\n sample,\n )\nspectra = list(read_spectra(sample))\nassert len(spectra) == 5\nprint(f\"Loaded {len(spectra)} MS/MS spectra\")"
38
+ },
39
+ {
40
+ "cell_type": "markdown",
41
+ "id": "rank-heading",
42
+ "metadata": {},
43
+ "source": "## Encode and rank\n\nThis small in-file ranking illustrates the Search representation. It is not an UltraAtlas search or a retrieval benchmark."
44
+ },
45
+ {
46
+ "cell_type": "code",
47
+ "id": "cosine-ranking",
48
+ "metadata": {},
49
+ "execution_count": null,
50
+ "outputs": [],
51
+ "source": "import numpy as np\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel = UltraMS.from_pretrained(\"search\", device=device)\nembeddings = model.encode_batch(spectra, batch_size=5)\nunit = embeddings / np.maximum(np.linalg.norm(embeddings, axis=1, keepdims=True), 1e-12)\nquery_index = 0\nscores = unit @ unit[query_index]\nranked = [index for index in np.argsort(-scores) if index != query_index]\nprint(\"Query:\", spectra[query_index][\"id\"])\nfor rank, index in enumerate(ranked, start=1):\n print(rank, spectra[index][\"id\"], float(scores[index]))"
52
+ }
53
+ ],
54
+ "metadata": {
55
+ "kernelspec": {
56
+ "display_name": "Python 3",
57
+ "language": "python",
58
+ "name": "python3"
59
+ },
60
+ "language_info": {
61
+ "name": "python"
62
+ },
63
+ "colab": {
64
+ "provenance": []
65
+ }
66
+ },
67
+ "nbformat": 4,
68
+ "nbformat_minor": 5
69
+ }
@@ -42,6 +42,21 @@ The `target` field is a numeric training value used by `model.batch_converter()`
42
42
 
43
43
  `model.encode(...).embedding` is a NumPy vector for inference. `model(peaks, attention_mask, precursor_mz)` returns a `[batch, embedding_dim]` PyTorch tensor with gradients for training. See [model selection](model-selection.md) for the representation returned by each checkpoint.
44
44
 
45
+ To inspect representations for individual spectral peaks, pass `return_peaks=True`:
46
+
47
+ ```python
48
+ result = model.encode(
49
+ mz=[150.0, 100.1, 121.1],
50
+ intensity=[35, 20, 100],
51
+ precursor_mz=301.2,
52
+ return_peaks=True,
53
+ )
54
+ print(result.peak_mz) # processed peak m/z, sorted ascending
55
+ print(result.peak_embeddings.shape) # (3, 1024) for the Unsupervised model
56
+ ```
57
+
58
+ `peak_embeddings[i]` is the encoder's final contextual representation for `peak_mz[i]` and `peak_intensity[i]`. These arrays reflect the peak selection, intensity normalization and sorting described above. They are returned only when requested.
59
+
45
60
  ## Spectrum files
46
61
 
47
62
  For MGF or `.mgf.gz`, UltraMS reads each `BEGIN IONS` block, uses `PEPMASS` for precursor-ion `m/z`, and takes the spectrum ID from `TITLE`, `NAME`, or `SCANS` when available. Extra columns after a spectral peak's `m/z` and intensity are ignored. The optional `ultrams[io]` installation adds mzML input; only MS2 spectra are read.
@@ -13,3 +13,16 @@ python -m pip install -e .
13
13
  | Rank spectra by cosine similarity | `python examples/spectrum_search.py` |
14
14
 
15
15
  Each command downloads its pretrained checkpoint from Hugging Face on first use. `batch_embeddings.py` saves an `.npz` containing spectrum IDs and one embedding per input spectrum. `spectrum_search.py` saves a CSV of ranked matches; its scores illustrate spectrum similarity and are not a benchmark. `pytorch_finetune.py` uses **placeholder regression targets based on file order** solely to show how to train the encoder and a task head. Replace these targets with measured labels for research. By default, output files go to a newly created temporary directory printed by each command; use `--output` or `--output-dir` to choose a persistent destination.
16
+
17
+ The PyTorch example saves both the encoder and task head in `model.pt`. Load them for prediction with:
18
+
19
+ ```python
20
+ import torch
21
+ from ultrams import UltraMS
22
+
23
+ weights = torch.load("/path/to/model.pt", map_location="cpu", weights_only=True)
24
+ model = UltraMS.from_pretrained("unsupervised").eval()
25
+ head = torch.nn.Linear(model.embedding_dim, 1).eval()
26
+ model.load_state_dict(weights["encoder"])
27
+ head.load_state_dict(weights["head"])
28
+ ```
@@ -47,6 +47,8 @@ def main() -> None:
47
47
 
48
48
  output_dir = args.output_dir or Path(tempfile.mkdtemp(prefix="ultrams-finetune-"))
49
49
  output_dir.mkdir(parents=True, exist_ok=True)
50
+ checkpoint = output_dir / "model.pt"
51
+ torch.save({"encoder": model.state_dict(), "head": head.state_dict()}, checkpoint)
50
52
  record = {
51
53
  "model": "unsupervised",
52
54
  "target": "demonstration labels from input order, scaled to [0, 1]",
@@ -56,6 +58,7 @@ def main() -> None:
56
58
  }
57
59
  (output_dir / "training.json").write_text(json.dumps(record, indent=2) + "\n")
58
60
  print(f"Trained on {len(spectra)} spectra with demonstration targets; final loss: {history[-1]:.4f}")
61
+ print(checkpoint)
59
62
  print(output_dir / "training.json")
60
63
 
61
64
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ultrams"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Pretrained UltraMS models for mass spectrometry"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -29,6 +29,9 @@ class SpectrumEmbedding:
29
29
  fusion: np.ndarray
30
30
  projection: np.ndarray | None = None
31
31
  rt_seconds: float | None = None
32
+ peak_embeddings: np.ndarray | None = None
33
+ peak_mz: np.ndarray | None = None
34
+ peak_intensity: np.ndarray | None = None
32
35
 
33
36
  @property
34
37
  def embedding(self) -> np.ndarray:
@@ -323,7 +326,18 @@ class UltraMS(torch.nn.Module):
323
326
 
324
327
  @torch.inference_mode()
325
328
  def encode(
326
- self, mz: Any, intensity: Any, *, precursor_mz: float
329
+ self, mz: Any, intensity: Any, *, precursor_mz: float, return_peaks: bool = False
330
+ ) -> SpectrumEmbedding:
331
+ """Encode one MS/MS spectrum; optionally return aligned peak embeddings."""
332
+ was_training = self.training
333
+ self.eval()
334
+ try:
335
+ return self._encode_one(mz, intensity, precursor_mz, return_peaks)
336
+ finally:
337
+ self.train(was_training)
338
+
339
+ def _encode_one(
340
+ self, mz: Any, intensity: Any, precursor_mz: float, return_peaks: bool
327
341
  ) -> SpectrumEmbedding:
328
342
  peaks, attention = _prepare_spectrum(mz, intensity, self.input_max_peaks)
329
343
  if not np.isfinite(precursor_mz) or precursor_mz <= 0:
@@ -356,4 +370,7 @@ class UltraMS(torch.nn.Module):
356
370
  fusion=fusion[0].cpu().numpy(),
357
371
  projection=projected[0].cpu().numpy() if projected is not None else None,
358
372
  rt_seconds=rt_seconds,
373
+ peak_embeddings=peak_hidden[0].float().cpu().numpy() if return_peaks else None,
374
+ peak_mz=peaks[:, 0].copy() if return_peaks else None,
375
+ peak_intensity=peaks[:, 1].copy() if return_peaks else None,
359
376
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ultrams
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Pretrained UltraMS models for mass spectrometry
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/Dsadd4/UltraMS
@@ -30,7 +30,8 @@ python -m pip install ultrams
30
30
  | Embed an MGF or mzML file | [File command](#embed-a-spectrum-file) |
31
31
  | Train on my labelled spectra | [PyTorch example](#fine-tune-with-pytorch) · [complete MGF example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) |
32
32
  | Compare spectra | [Search example](https://github.com/Dsadd4/UltraMS/blob/main/examples/spectrum_search.py) |
33
- | Reproduce pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
33
+ | Learn in a notebook | [UltraMS tutorials](https://github.com/Dsadd4/UltraMS/tree/main/cookbook/tutorials) |
34
+ | Inspect or run pretraining | [Training code](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) |
34
35
 
35
36
  ## Embed a spectrum
36
37
 
@@ -48,6 +49,8 @@ print(embedding.shape) # (1024,)
48
49
 
49
50
  The first call downloads the selected checkpoint from Hugging Face. Use `device="cuda"` or `device="mps"` with `from_pretrained` to run on an available accelerator.
50
51
 
52
+ The three checkpoints are collected on [Hugging Face](https://huggingface.co/collections/dsadd4/ultrams-6ab4cfaa860cbbd1f9a6b9b6).
53
+
51
54
  ## Embed a spectrum file
52
55
 
53
56
  ```bash
@@ -87,6 +90,8 @@ for batch in loader:
87
90
 
88
91
  The [complete fine-tuning example](https://github.com/Dsadd4/UltraMS/blob/main/examples/pytorch_finetune.py) reads an MGF file and saves training history. For a shorter route, use `model.finetune(labelled_spectra, task="regression")` with a `label` field; it saves weights, configuration, and losses. See [spectrum input](https://github.com/Dsadd4/UltraMS/blob/main/docs/data-format.md) for the input format.
89
92
 
93
+ For measured molecular labels, the [MassSpecGym contrastive tutorial](https://github.com/Dsadd4/UltraMS/blob/main/cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md) downloads the dataset, keeps its train/validation/test folds, fine-tunes the encoder, and reloads the selected checkpoint.
94
+
90
95
  ## Choose a model
91
96
 
92
97
  | Model name | Representation | Dimension | Spectral peak limit | Weights |
@@ -100,3 +105,7 @@ Load any of them with `UltraMS.from_pretrained("name")`, or load a downloaded `m
100
105
  ## Pretraining
101
106
 
102
107
  The [UltraMSdata pretraining package](https://github.com/Dsadd4/UltraMS/blob/main/training/README.md) contains the current model architecture, training entry points, configuration and history outputs. It installs separately from the inference package.
108
+
109
+ ## Cite
110
+
111
+ Citation metadata for this software is in [CITATION.cff](https://github.com/Dsadd4/UltraMS/blob/main/CITATION.cff).
@@ -1,7 +1,14 @@
1
+ CITATION.cff
1
2
  LICENSE
2
3
  MANIFEST.in
3
4
  README.md
4
5
  pyproject.toml
6
+ cookbook/tutorials/MASSSPECGYM_CONTRASTIVE.md
7
+ cookbook/tutorials/README.md
8
+ cookbook/tutorials/embed.ipynb
9
+ cookbook/tutorials/massspecgym_contrastive.py
10
+ cookbook/tutorials/pytorch_finetune.ipynb
11
+ cookbook/tutorials/spectrum_search.ipynb
5
12
  docs/data-format.md
6
13
  docs/model-selection.md
7
14
  examples/README.md
@@ -53,6 +53,21 @@ class ModelAPITests(unittest.TestCase):
53
53
  actual = self.model.encode_batch([], batch_size=2)
54
54
  self.assertEqual(actual.shape, (0, self.model.embedding_dim))
55
55
 
56
+ def test_peak_embeddings_align_with_selected_sorted_peaks(self) -> None:
57
+ mz = [180, 100, 150, 110, 140, 120, 170, 130, 190, 160]
58
+ intensity = [8, 1, 5, 2, 4, 3, 7, 6, 10, 9]
59
+ result = self.model.encode(mz, intensity, precursor_mz=301.2, return_peaks=True)
60
+ self.assertEqual(result.peak_embeddings.shape, (8, 16))
61
+ np.testing.assert_array_equal(
62
+ result.peak_mz,
63
+ np.asarray([120, 130, 140, 150, 160, 170, 180, 190], dtype=np.float32),
64
+ )
65
+ np.testing.assert_array_equal(
66
+ result.peak_intensity,
67
+ np.asarray([3, 6, 4, 5, 9, 7, 8, 10], dtype=np.float32) / 10,
68
+ )
69
+ self.assertIsNone(self.model.encode(mz, intensity, precursor_mz=301.2).peak_embeddings)
70
+
56
71
  def test_real_mgf_to_npz_with_local_checkpoint(self) -> None:
57
72
  source = Path(__file__).resolve().parents[1] / "examples/data/example_5_spectra.mgf"
58
73
  with tempfile.TemporaryDirectory() as directory:
ultrams-0.2.0/MANIFEST.in DELETED
@@ -1,2 +0,0 @@
1
- recursive-include docs *.md
2
- recursive-include examples *.py *.md *.mgf
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes