bilalsm commited on
Commit
211dde8
·
verified ·
1 Parent(s): 06b5da2

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ architecture.png filter=lfs diff=lfs merge=lfs -text
DecisionTree.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a2ec37a3f117ef6cd281d12ffbf2a91779513a4c80b230ea7def4552628e57df
3
+ size 4984593
README.md CHANGED
@@ -14,7 +14,7 @@ datasets:
14
  # DOM Formula Assignment using Machine Learning
15
 
16
 
17
- ![Model Type](https://img.shields.io/badge/Model-KNN-blue)
18
  ![Data](https://img.shields.io/badge/Data-FT--ICR_MS-green)
19
  ![License](https://img.shields.io/badge/License-CC_BY_NC_ND_4-yellow)
20
  [![GitHub](https://img.shields.io/badge/GitHub-pcdslab/dom--formula--assignment--using--ml-blue?logo=github)](https://github.com/pcdslab/dom-formula-assignment-using-ml)
@@ -30,47 +30,153 @@ datasets:
30
  ## Abstract
31
  A machine learning approach to molecular formula assignment is crucial for unlocking the full potential of ultra-high resolution mass spectrometry (UHRMS) when analyzing complex mixtures. By combining data-driven models with rigorous benchmarking, the accuracy, consistency, and speed in identifying plausible molecular formulas from vast spectral datasets can be improved. Compared with traditional de novo methods that rely heavily on rule-based heuristics, and manual parameter tuning, machine learning approaches can capture complex patterns in data and adapt more readily to diverse sample types. In this paper, we describe the application of a machine learning methods using the k-nearest neighbors (KNN) algorithm trained on curated chemical formula datasets of UHRMS analysis of dissolved organic matter (DOM) covering the saline river continuum and tropical wet/dry season variability. The influence of the mass accuracy (training set with 0.15-1ppm) was evaluated on a blind test set of DOMs of different geographical origins. A Decision Tree Regressor (DTR) and Random Forest Regressor (RFR) based on mass accuracy (<1ppm) was used. Results from our ML models exhibit 43% more formulas annotated than traditional methods (5796 vs 4047), Model-Synthetic achieved 99.9% assignment rate and annotated/assigned 2x more formulas (8,268 vs 4047). DTR and RFR achieved formula-level accuracies (FA) of 86.5% and 60.4%, respectively. Overall, results show an increase in formula assignment when compared with traditional methods. This ultimately enables more reliable characterization of complex natural and engineered systems, supporting advances in fields such as environmental science, metabolomics, and petroleomics. Furthermore, the novel data set produced for this study is made publicly available, establishing an initial benchmark for molecular formula assignment in UHRMS using machine learning. The dataset and code are publicly available at: https://github.com/pcdslab/dom-formula-assignment-using-ml
32
 
33
- ## Available Models
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  This repository contains pre-trained KNN models used for molecular formula assignment. The models vary by:
36
- - **Dataset**: L1, L3, L1-L3, Synthetic
37
  - **Neighbors (K)**: 1, 3
38
  - **Distance Metric**: Euclidean, Manhattan
39
- - **Field Strength/Type**: 7T, 21T, SYN (Synthetic)
40
 
41
  ### Model List
42
 
43
- | Model Filename | Description |
44
  |---|---|
45
- | `knn_model_Model-L1_K1_Euclidean.joblib` | L1 Dataset, K=1, Euclidean Distance |
46
- | `knn_model_Model-L1_K1_Manhattan.joblib` | L1 Dataset, K=1, Manhattan Distance |
47
- | `knn_model_Model-L1_K3_Euclidean.joblib` | L1 Dataset, K=3, Euclidean Distance |
48
- | `knn_model_Model-L1_K3_Manhattan.joblib` | L1 Dataset, K=3, Manhattan Distance |
49
- | `knn_model_Model-L3_K1_Euclidean.joblib` | L3 Dataset, K=1, Euclidean Distance |
50
- | `knn_model_Model-L3_K1_Manhattan.joblib` | L3 Dataset, K=1, Manhattan Distance |
51
- | `knn_model_Model-L3_K3_Euclidean.joblib` | L3 Dataset, K=3, Euclidean Distance |
52
- | `knn_model_Model-L3_K3_Manhattan.joblib` | L3 Dataset, K=3, Manhattan Distance |
53
- | `knn_model_Model-L1-L3_K1_Euclidean_21T.joblib` | L1-L3 Combined, K=1, Euclidean, 21T |
54
- | `knn_model_Model-L1-L3_K1_Euclidean_7T.joblib` | L1-L3 Combined, K=1, Euclidean, 7T |
55
- | `knn_model_Model-L1-L3_K1_Manhattan_21T.joblib` | L1-L3 Combined, K=1, Manhattan, 21T |
56
- | `knn_model_Model-L1-L3_K1_Manhattan_7T.joblib` | L1-L3 Combined, K=1, Manhattan, 7T |
57
- | `knn_model_Model-L1-L3_K3_Euclidean_21T.joblib` | L1-L3 Combined, K=3, Euclidean, 21T |
58
- | `knn_model_Model-L1-L3_K3_Euclidean_7T.joblib` | L1-L3 Combined, K=3, Euclidean, 7T |
59
- | `knn_model_Model-L1-L3_K3_Manhattan_21T.joblib` | L1-L3 Combined, K=3, Manhattan, 21T |
60
- | `knn_model_Model-L1-L3_K3_Manhattan_7T.joblib` | L1-L3 Combined, K=3, Manhattan, 7T |
61
- | `knn_model_Model-Synthetic_K1_Euclidean_21T.joblib` | Synthetic, K=1, Euclidean, 21T |
62
- | `knn_model_Model-Synthetic_K1_Euclidean_7T.joblib` | Synthetic, K=1, Euclidean, 7T |
63
- | `knn_model_Model-Synthetic_K1_Euclidean_SYN.joblib` | Synthetic, K=1, Euclidean, SYN |
64
- | `knn_model_Model-Synthetic_K1_Manhattan_21T.joblib` | Synthetic, K=1, Manhattan, 21T |
65
- | `knn_model_Model-Synthetic_K1_Manhattan_7T.joblib` | Synthetic, K=1, Manhattan, 7T |
66
- | `knn_model_Model-Synthetic_K1_Manhattan_SYN.joblib` | Synthetic, K=1, Manhattan, SYN |
67
- | `knn_model_Model-Synthetic_K3_Euclidean_21T.joblib` | Synthetic, K=3, Euclidean, 21T |
68
- | `knn_model_Model-Synthetic_K3_Euclidean_7T.joblib` | Synthetic, K=3, Euclidean, 7T |
69
- | `knn_model_Model-Synthetic_K3_Euclidean_SYN.joblib` | Synthetic, K=3, Euclidean, SYN |
70
- | `knn_model_Model-Synthetic_K3_Manhattan_21T.joblib` | Synthetic, K=3, Manhattan, 21T |
71
- | `knn_model_Model-Synthetic_K3_Manhattan_7T.joblib` | Synthetic, K=3, Manhattan, 7T |
72
- | `knn_model_Model-Synthetic_K3_Manhattan_SYN.joblib` | Synthetic, K=3, Manhattan, SYN |
73
-
74
 
75
  ## License
76
 
 
14
  # DOM Formula Assignment using Machine Learning
15
 
16
 
17
+ ![Model Type](https://img.shields.io/badge/Model-ML-blue)
18
  ![Data](https://img.shields.io/badge/Data-FT--ICR_MS-green)
19
  ![License](https://img.shields.io/badge/License-CC_BY_NC_ND_4-yellow)
20
  [![GitHub](https://img.shields.io/badge/GitHub-pcdslab/dom--formula--assignment--using--ml-blue?logo=github)](https://github.com/pcdslab/dom-formula-assignment-using-ml)
 
30
  ## Abstract
31
  A machine learning approach to molecular formula assignment is crucial for unlocking the full potential of ultra-high resolution mass spectrometry (UHRMS) when analyzing complex mixtures. By combining data-driven models with rigorous benchmarking, the accuracy, consistency, and speed in identifying plausible molecular formulas from vast spectral datasets can be improved. Compared with traditional de novo methods that rely heavily on rule-based heuristics, and manual parameter tuning, machine learning approaches can capture complex patterns in data and adapt more readily to diverse sample types. In this paper, we describe the application of a machine learning methods using the k-nearest neighbors (KNN) algorithm trained on curated chemical formula datasets of UHRMS analysis of dissolved organic matter (DOM) covering the saline river continuum and tropical wet/dry season variability. The influence of the mass accuracy (training set with 0.15-1ppm) was evaluated on a blind test set of DOMs of different geographical origins. A Decision Tree Regressor (DTR) and Random Forest Regressor (RFR) based on mass accuracy (<1ppm) was used. Results from our ML models exhibit 43% more formulas annotated than traditional methods (5796 vs 4047), Model-Synthetic achieved 99.9% assignment rate and annotated/assigned 2x more formulas (8,268 vs 4047). DTR and RFR achieved formula-level accuracies (FA) of 86.5% and 60.4%, respectively. Overall, results show an increase in formula assignment when compared with traditional methods. This ultimately enables more reliable characterization of complex natural and engineered systems, supporting advances in fields such as environmental science, metabolomics, and petroleomics. Furthermore, the novel data set produced for this study is made publicly available, establishing an initial benchmark for molecular formula assignment in UHRMS using machine learning. The dataset and code are publicly available at: https://github.com/pcdslab/dom-formula-assignment-using-ml
32
 
33
+ ## Architecture
34
+
35
+ ![DOM formula assignment architecture](architecture.png)
36
+ (a) KNN Pipeline for Formula Assignment/Annotation. For each train set, the first KNN model is fitted, and then formula assignment/annotation is performed using the closest peaks in the training set. (b) Decision Tree Regressor (DTR) and Random Forest Regressor (RFR) are trained to predict element counts for CHONS using squared-error criterion.
37
+ ## Usage
38
+
39
+ Install the dependencies:
40
+
41
+ ```bash
42
+ pip install "numpy==2.4.1" "scikit-learn==1.8.0" joblib huggingface_hub transformers torch pandas
43
+ ```
44
+
45
+
46
+ ### KNN Formula Assignment
47
+
48
+ Load the default KNN model:
49
+
50
+ ```python
51
+ from transformers import AutoModel
52
+ import torch
53
+
54
+ model = AutoModel.from_pretrained(
55
+ "SaeedLab/dom-formula-assignment-using-ml",
56
+ trust_remote_code=True,
57
+ ).eval()
58
+
59
+ # Mass-only input. Each value is one mass to assign.
60
+ masses = torch.tensor([350.123456, 421.987654], dtype=torch.float64)
61
+
62
+ with torch.no_grad():
63
+ outputs = model(features=masses, return_neighbors=True, n_neighbors=3)
64
+
65
+ print("Predicted formulas:", outputs.predictions)
66
+ print("Neighbor indices:", outputs.indices)
67
+ ```
68
+
69
+ Load a specific KNN model by passing `model_name`:
70
+
71
+ ```python
72
+ from transformers import AutoModel
73
+
74
+ model = AutoModel.from_pretrained(
75
+ "SaeedLab/dom-formula-assignment-using-ml",
76
+ trust_remote_code=True,
77
+ model_name="L1-L3_K3_Manhattan_Ensemble",
78
+ ).eval()
79
+ ```
80
+
81
+ Use the `Model Name` value from the table below. For example, `Synthetic_K3_Euclidean_Ensemble` selects the default synthetic KNN ensemble.
82
+
83
+ There is no tokenizer step for these KNN models. Inputs are 1D numeric vectors of mass values.
84
+
85
+ ### Get nearest neighbors
86
+
87
+ KNN models can also return the closest training examples for each input sample:
88
+
89
+ ```python
90
+ neighbor_indices = model.neighbor_indices(masses, n_neighbors=3)
91
+
92
+ print("Neighbor indices:")
93
+ print(neighbor_indices)
94
+ ```
95
+
96
+ Use the model variant that matches your experiment setup:
97
+
98
+ - `L1`, `L3`, `L1-L3`, or `Synthetic` for the training dataset.
99
+ - `K1` or `K3` for the number of neighbors.
100
+ - `Euclidean` or `Manhattan` for the distance metric.
101
+ - `Ensemble`, `7T`, `21T`, or `SYN` for combined, field-strength, or synthetic backing models.
102
+
103
+ ### Decision Tree and Random Forest Formula Regressors
104
+
105
+ The Decision Tree and Random Forest models predict CHONS element counts from mass and ion mobility features. Inputs must be numeric rows in this order:
106
+
107
+ ```text
108
+ [mz, inv_k0, ccs]
109
+ ```
110
+
111
+ Here, `inv_k0` is `1/k0`, and both `inv_k0` and `ccs` are mobility-derived values.
112
+
113
+ ```python
114
+ from transformers import AutoModel
115
+ import torch
116
+
117
+ model = AutoModel.from_pretrained(
118
+ "SaeedLab/dom-formula-assignment-using-ml",
119
+ trust_remote_code=True,
120
+ model_name="RandomForest",
121
+ ).eval()
122
+
123
+ features = torch.tensor(
124
+ [
125
+ [191.03498, 0.6808667247437136, 146.2048497276655],
126
+ [191.071388, 0.7087887538442335, 152.19879035313514],
127
+ [191.10775, 0.6935974811483341, 148.93494377261592],
128
+ ],
129
+ dtype=torch.float64,
130
+ )
131
+
132
+ with torch.no_grad():
133
+ outputs = model(features=features)
134
+
135
+ print("Predicted formulas:", outputs.formulas)
136
+ print("Predicted CHONS counts:", outputs.formula_counts)
137
+ ```
138
+
139
+ The CHONS output columns are ordered as:
140
+
141
+ ```text
142
+ [C, H, O, N, S]
143
+ ```
144
+
145
+ ## KNN Models
146
 
147
  This repository contains pre-trained KNN models used for molecular formula assignment. The models vary by:
148
+ - **Dataset**: L1 (7T), L3 (21T), L1-L3, Synthetic
149
  - **Neighbors (K)**: 1, 3
150
  - **Distance Metric**: Euclidean, Manhattan
151
+ - **Field Strength/Type**: Ensemble, 7T, 21T, SYN (Synthetic)
152
 
153
  ### Model List
154
 
155
+ | Model Name | Description |
156
  |---|---|
157
+ | `L1_K1_Euclidean` | L1 (7T) KNN model with 1 neighbor and Euclidean distance |
158
+ | `L1_K1_Manhattan` | L1 (7T) KNN model with 1 neighbor and Manhattan distance |
159
+ | `L1_K3_Euclidean` | L1 (7T) KNN model with 3 neighbors and Euclidean distance |
160
+ | `L1_K3_Manhattan` | L1 (7T) KNN model with 3 neighbors and Manhattan distance |
161
+ | `L3_K1_Euclidean` | L3 (21T) KNN model with 1 neighbor and Euclidean distance |
162
+ | `L3_K1_Manhattan` | L3 (21T) KNN model with 1 neighbor and Manhattan distance |
163
+ | `L3_K3_Euclidean` | L3 (21T) KNN model with 3 neighbors and Euclidean distance |
164
+ | `L3_K3_Manhattan` | L3 (21T) KNN model with 3 neighbors and Manhattan distance |
165
+ | `L1-L3_K1_Euclidean_Ensemble` | Ensemble of L1 (7T) and L3 (21T) KNN models with 1 neighbor and Euclidean distance |
166
+ | `L1-L3_K1_Manhattan_Ensemble` | Ensemble of L1 (7T) and L3 (21T) KNN models with 1 neighbor and Manhattan distance |
167
+ | `L1-L3_K3_Euclidean_Ensemble` | Ensemble of L1 (7T) and L3 (21T) KNN models with 3 neighbors and Euclidean distance |
168
+ | `L1-L3_K3_Manhattan_Ensemble` | Ensemble of L1 (7T) and L3 (21T) KNN models with 3 neighbors and Manhattan distance |
169
+ | `Synthetic_K1_Euclidean_Ensemble` | Ensemble of Synthetic 7T, 21T, and SYN KNN models with 1 neighbor and Euclidean distance |
170
+ | `Synthetic_K1_Manhattan_Ensemble` | Ensemble of Synthetic 7T, 21T, and SYN KNN models with 1 neighbor and Manhattan distance |
171
+ | `Synthetic_K3_Euclidean_Ensemble` | Ensemble of Synthetic 7T, 21T, and SYN KNN models with 3 neighbors and Euclidean distance |
172
+ | `Synthetic_K3_Manhattan_Ensemble` | Ensemble of Synthetic 7T, 21T, and SYN KNN models with 3 neighbors and Manhattan distance |
173
+
174
+ ## Decision Tree and Random Forest Models
175
+
176
+ | Model Name | Description |
177
+ |---|---|
178
+ | `DecisionTree` | Predicts CHONS counts from `mz`, `inv_k0`, and `ccs` |
179
+ | `RandomForest` | Predicts CHONS counts from `mz`, `inv_k0`, and `ccs` |
 
 
 
 
 
 
180
 
181
  ## License
182
 
RandomForest.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b6b93e850427cebb857a42e7714b25ae672425ee644ffae03a7d3d52b97adcfb
3
+ size 233828641
architecture.png ADDED

Git LFS Details

  • SHA256: db2a386d75a92cb9ffab66d7714d08fca682797d1b09dd36f7ca0b20ddcdd252
  • Pointer size: 132 Bytes
  • Size of remote file: 1.65 MB
config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DomMLModel"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_dom_ml.DomMLConfig",
7
+ "AutoModel": "modeling_dom_ml.DomMLModel"
8
+ },
9
+ "feature_names": [
10
+ "mass"
11
+ ],
12
+ "library_name": "transformers",
13
+ "model_name": "Synthetic_K3_Euclidean_Ensemble",
14
+ "model_type": "dom_ml"
15
+ }
configuration_dom_ml.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class DomMLConfig(PretrainedConfig):
5
+ model_type = "dom_ml"
6
+
7
+ def __init__(
8
+ self,
9
+ model_name="Synthetic_K3_Euclidean_Ensemble",
10
+ model_file=None,
11
+ feature_names=None,
12
+ model_kind=None,
13
+ **kwargs,
14
+ ):
15
+ super().__init__(**kwargs)
16
+ self.model_name = model_name
17
+ self.model_file = model_file
18
+ self.model_kind = model_kind
19
+ self.feature_names = feature_names or ["mass"]
modeling_dom_ml.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ from huggingface_hub import hf_hub_download
4
+ from joblib import load
5
+ from transformers import PreTrainedModel
6
+ from transformers.utils import ModelOutput
7
+ import os
8
+ from dataclasses import dataclass
9
+ from typing import Any, Optional
10
+
11
+ from .configuration_dom_ml import DomMLConfig
12
+
13
+
14
+ ELEMENTS = ("C", "H", "O", "N", "S")
15
+ FORMULA_REGRESSOR_NAMES = {"DecisionTree", "RandomForest"}
16
+
17
+
18
+ @dataclass
19
+ class DomMLOutput(ModelOutput):
20
+ predictions: Any = None
21
+ formula_counts: Any = None
22
+ formulas: Any = None
23
+ distances: Optional[Any] = None
24
+ indices: Optional[Any] = None
25
+
26
+
27
+ class DomMLModel(PreTrainedModel):
28
+ config_class = DomMLConfig
29
+ base_model_prefix = "dom_ml"
30
+ main_input_name = "features"
31
+
32
+ def __init__(self, config, estimator=None, estimators=None):
33
+ super().__init__(config)
34
+ self.estimators = estimators or ([estimator] if estimator is not None else [])
35
+ self.estimator = self.estimators[0] if self.estimators else None
36
+
37
+ @staticmethod
38
+ def _model_name_to_file(model_name):
39
+ if not model_name:
40
+ return None
41
+ if model_name in FORMULA_REGRESSOR_NAMES:
42
+ return f"{model_name}.joblib"
43
+ if model_name.endswith(".joblib"):
44
+ return model_name
45
+ if model_name.startswith("knn_model_"):
46
+ return f"{model_name}.joblib"
47
+ if model_name.startswith("Model-"):
48
+ return f"knn_model_{model_name}.joblib"
49
+ return f"knn_model_Model-{model_name}.joblib"
50
+
51
+ @classmethod
52
+ def _model_name_to_files(cls, model_name):
53
+ if not model_name:
54
+ return None
55
+ if model_name.startswith("L1-L3_") and model_name.endswith("_Ensemble"):
56
+ base_name = model_name[: -len("_Ensemble")]
57
+ return [
58
+ cls._model_name_to_file(f"{base_name}_7T"),
59
+ cls._model_name_to_file(f"{base_name}_21T"),
60
+ ]
61
+ if model_name.startswith("Synthetic_") and model_name.endswith("_Ensemble"):
62
+ base_name = model_name[: -len("_Ensemble")]
63
+ return [
64
+ cls._model_name_to_file(f"{base_name}_7T"),
65
+ cls._model_name_to_file(f"{base_name}_21T"),
66
+ cls._model_name_to_file(f"{base_name}_SYN"),
67
+ ]
68
+ return [cls._model_name_to_file(model_name)]
69
+
70
+ @staticmethod
71
+ def _infer_model_kind(model_name, model_files):
72
+ if model_name in FORMULA_REGRESSOR_NAMES:
73
+ return "formula_regressor"
74
+ if model_files and all(
75
+ os.path.basename(model_file) in {"DecisionTree.joblib", "RandomForest.joblib"}
76
+ for model_file in model_files
77
+ ):
78
+ return "formula_regressor"
79
+ return "knn"
80
+
81
+ @classmethod
82
+ def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
83
+ config = kwargs.pop("config", None)
84
+ model_name = kwargs.pop("model_name", None)
85
+ model_file = kwargs.pop("model_file", None)
86
+ cache_dir = kwargs.pop("cache_dir", None)
87
+ force_download = kwargs.pop("force_download", False)
88
+ local_files_only = kwargs.pop("local_files_only", False)
89
+ token = kwargs.pop("token", None)
90
+ revision = kwargs.pop("revision", None)
91
+ subfolder = kwargs.pop("subfolder", "")
92
+
93
+ kwargs.pop("trust_remote_code", None)
94
+ kwargs.pop("code_revision", None)
95
+ kwargs.pop("_commit_hash", None)
96
+
97
+ if config is None:
98
+ config = DomMLConfig.from_pretrained(
99
+ pretrained_model_name_or_path,
100
+ cache_dir=cache_dir,
101
+ force_download=force_download,
102
+ local_files_only=local_files_only,
103
+ token=token,
104
+ revision=revision,
105
+ subfolder=subfolder,
106
+ )
107
+
108
+ model_files = None
109
+ if model_file is not None:
110
+ model_files = [model_file]
111
+ else:
112
+ model_name = model_name or getattr(config, "model_name", None)
113
+ model_files = cls._model_name_to_files(model_name)
114
+ if model_files is None:
115
+ configured_model_file = getattr(config, "model_file", None)
116
+ if configured_model_file is not None:
117
+ model_files = [configured_model_file]
118
+ if model_files is None:
119
+ raise ValueError("Pass model_name=... to select one of the available models.")
120
+
121
+ config.model_name = model_name
122
+ config.model_file = model_files[0]
123
+ config.model_files = model_files
124
+ config.model_kind = cls._infer_model_kind(model_name, model_files)
125
+ if config.model_kind == "formula_regressor":
126
+ config.feature_names = ["mz", "inv_k0", "ccs"]
127
+
128
+ model_paths = []
129
+ for current_model_file in model_files:
130
+ if os.path.isdir(pretrained_model_name_or_path):
131
+ model_path = os.path.join(
132
+ pretrained_model_name_or_path,
133
+ subfolder,
134
+ current_model_file,
135
+ )
136
+ else:
137
+ model_path = hf_hub_download(
138
+ repo_id=pretrained_model_name_or_path,
139
+ filename=current_model_file,
140
+ cache_dir=cache_dir,
141
+ force_download=force_download,
142
+ local_files_only=local_files_only,
143
+ token=token,
144
+ revision=revision,
145
+ subfolder=subfolder,
146
+ )
147
+ model_paths.append(model_path)
148
+
149
+ estimators = [load(model_path) for model_path in model_paths]
150
+ model = cls(config, estimators=estimators)
151
+ model.eval()
152
+ return model
153
+
154
+ def _to_numpy(self, features):
155
+ if isinstance(features, torch.Tensor):
156
+ values = features.detach().cpu().numpy()
157
+ elif hasattr(features, "loc") and hasattr(features, "columns"):
158
+ feature_names = getattr(self.config, "feature_names", None)
159
+ if feature_names and all(name in features.columns for name in feature_names):
160
+ values = features.loc[:, feature_names].to_numpy(dtype=np.float64)
161
+ else:
162
+ values = features.to_numpy(dtype=np.float64)
163
+ elif hasattr(features, "to_numpy"):
164
+ values = features.to_numpy(dtype=np.float64)
165
+ else:
166
+ values = np.asarray(features, dtype=np.float64)
167
+
168
+ if getattr(self.config, "model_kind", None) == "formula_regressor":
169
+ if values.ndim == 1:
170
+ if values.size != 3:
171
+ raise ValueError(
172
+ "DecisionTree and RandomForest inputs must use [mz, inv_k0, ccs]."
173
+ )
174
+ return values.reshape(1, 3)
175
+ return values
176
+
177
+ if values.ndim == 0:
178
+ return values.reshape(1, 1)
179
+ if values.ndim == 1:
180
+ return values.reshape(-1, 1)
181
+ return values
182
+
183
+ @staticmethod
184
+ def _maybe_tensor(array, as_numpy, dtype=None):
185
+ if as_numpy or array is None:
186
+ return array
187
+ return torch.as_tensor(array, dtype=dtype)
188
+
189
+ @staticmethod
190
+ def _counts_to_formulas(counts):
191
+ formulas = []
192
+ for row in np.asarray(counts, dtype=int):
193
+ formula = ""
194
+ for element, count in zip(ELEMENTS, row):
195
+ if count > 0:
196
+ formula += element
197
+ if count != 1:
198
+ formula += str(int(count))
199
+ formulas.append(formula)
200
+ return np.asarray(formulas)
201
+
202
+ def predict_counts(self, features, as_numpy=True):
203
+ if not self.estimators:
204
+ raise ValueError("No model is loaded.")
205
+ if getattr(self.config, "model_kind", None) != "formula_regressor":
206
+ raise ValueError("predict_counts is only available for DecisionTree and RandomForest.")
207
+
208
+ values = self._to_numpy(features)
209
+ raw_counts = self.estimators[0].predict(values)
210
+ counts = np.rint(raw_counts).astype(int)
211
+ counts = np.clip(counts, 0, None)
212
+ if as_numpy:
213
+ return counts
214
+ return torch.as_tensor(counts, dtype=torch.long)
215
+
216
+ def predict(self, features):
217
+ if not self.estimators:
218
+ raise ValueError("No model is loaded.")
219
+ if getattr(self.config, "model_kind", None) == "formula_regressor":
220
+ return self._counts_to_formulas(self.predict_counts(features))
221
+
222
+ values = self._to_numpy(features)
223
+ predictions = [model.predict(values) for model in self.estimators]
224
+ if len(predictions) == 1:
225
+ return predictions[0]
226
+
227
+ stacked = np.vstack(predictions).T
228
+ voted_predictions = []
229
+ for row in stacked:
230
+ counts = {}
231
+ for prediction in row:
232
+ counts[prediction] = counts.get(prediction, 0) + 1
233
+ voted_predictions.append(
234
+ max(row, key=lambda prediction: counts[prediction])
235
+ )
236
+ return np.asarray(voted_predictions)
237
+
238
+ def joblib_summary(self, max_items=5):
239
+ if not self.estimators:
240
+ raise ValueError("No model is loaded.")
241
+
242
+ summaries = []
243
+ model_files = getattr(self.config, "model_files", None) or [self.config.model_file]
244
+ for model_file, estimator in zip(model_files, self.estimators):
245
+ summary = {
246
+ "type": type(estimator).__name__,
247
+ "model_file": model_file,
248
+ "model_kind": getattr(self.config, "model_kind", None),
249
+ "feature_names": getattr(self.config, "feature_names", None),
250
+ "n_features_in": getattr(estimator, "n_features_in_", None),
251
+ "n_samples_fit": getattr(estimator, "n_samples_fit_", None),
252
+ }
253
+ if getattr(self.config, "model_kind", None) == "formula_regressor":
254
+ summary["output_elements"] = list(ELEMENTS)
255
+
256
+ classes = getattr(estimator, "classes_", None)
257
+ if classes is not None:
258
+ summary["classes_preview"] = classes[:max_items].tolist()
259
+
260
+ fit_x = getattr(estimator, "_fit_X", None)
261
+ if fit_x is not None:
262
+ summary["fit_masses_preview"] = np.asarray(fit_x[:max_items]).reshape(-1).tolist()
263
+
264
+ summaries.append(summary)
265
+
266
+ if len(summaries) == 1:
267
+ return summaries[0]
268
+ return {
269
+ "type": "Ensemble",
270
+ "model_name": self.config.model_name,
271
+ "models": summaries,
272
+ }
273
+
274
+ def kneighbors(self, features, n_neighbors=None, as_numpy=False):
275
+ if getattr(self.config, "model_kind", None) == "formula_regressor":
276
+ raise ValueError("Nearest-neighbor lookup is only available for KNN models.")
277
+ if not self.estimators:
278
+ raise ValueError("No KNN model is loaded.")
279
+ values = self._to_numpy(features)
280
+ if len(self.estimators) > 1:
281
+ model_files = getattr(self.config, "model_files", None) or []
282
+ distances = {}
283
+ indices = {}
284
+ for model_file, estimator in zip(model_files, self.estimators):
285
+ model_distances, model_indices = estimator.kneighbors(
286
+ values,
287
+ n_neighbors=n_neighbors,
288
+ )
289
+ distances[model_file] = self._maybe_tensor(
290
+ model_distances,
291
+ as_numpy,
292
+ dtype=torch.float32,
293
+ )
294
+ indices[model_file] = self._maybe_tensor(
295
+ model_indices,
296
+ as_numpy,
297
+ dtype=torch.long,
298
+ )
299
+ return distances, indices
300
+
301
+ distances, indices = self.estimators[0].kneighbors(
302
+ values,
303
+ n_neighbors=n_neighbors,
304
+ )
305
+ return (
306
+ self._maybe_tensor(distances, as_numpy, dtype=torch.float32),
307
+ self._maybe_tensor(indices, as_numpy, dtype=torch.long),
308
+ )
309
+
310
+ def neighbor_indices(self, features, n_neighbors=None, as_numpy=False):
311
+ _, indices = self.kneighbors(
312
+ features,
313
+ n_neighbors=n_neighbors,
314
+ as_numpy=as_numpy,
315
+ )
316
+ return indices
317
+
318
+ def forward(
319
+ self,
320
+ features=None,
321
+ input_features=None,
322
+ return_neighbors=False,
323
+ n_neighbors=None,
324
+ as_numpy=False,
325
+ **kwargs,
326
+ ):
327
+ if features is None:
328
+ features = input_features
329
+ if features is None:
330
+ raise ValueError("Pass model inputs with features=... or input_features=....")
331
+
332
+ predictions = self.predict(features)
333
+ formula_counts = None
334
+ formulas = None
335
+ distances = None
336
+ indices = None
337
+
338
+ if getattr(self.config, "model_kind", None) == "formula_regressor":
339
+ formula_counts = self.predict_counts(features, as_numpy=as_numpy)
340
+ formulas = predictions
341
+ elif return_neighbors:
342
+ distances, indices = self.kneighbors(
343
+ features,
344
+ n_neighbors=n_neighbors,
345
+ as_numpy=as_numpy,
346
+ )
347
+
348
+ return DomMLOutput(
349
+ predictions=predictions,
350
+ formula_counts=formula_counts,
351
+ formulas=formulas,
352
+ distances=distances,
353
+ indices=indices,
354
+ )
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ numpy==2.4.1
2
+ scikit-learn==1.8.0
3
+ joblib
4
+ huggingface_hub
5
+ transformers
6
+ torch
7
+ pandas