alf_tools 0.1.0a0__py3-none-any.whl

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 (46) hide show
  1. alf_tools/__init__.py +17 -0
  2. alf_tools/datasets/__init__.py +28 -0
  3. alf_tools/datasets/flip.py +312 -0
  4. alf_tools/datasets/gfp.py +73 -0
  5. alf_tools/datasets/guacamol/__init__.py +17 -0
  6. alf_tools/datasets/guacamol/guacamol_dataset.py +482 -0
  7. alf_tools/datasets/guacamol/guacamol_scoring.py +587 -0
  8. alf_tools/datasets/guacamol/guacamol_utils.py +375 -0
  9. alf_tools/datasets/proteingym.py +323 -0
  10. alf_tools/models/__init__.py +84 -0
  11. alf_tools/models/chemprop.py +475 -0
  12. alf_tools/models/cnn.py +674 -0
  13. alf_tools/models/ensemble.py +350 -0
  14. alf_tools/models/esm2.py +902 -0
  15. alf_tools/models/esm_utils/__init__.py +15 -0
  16. alf_tools/models/esm_utils/config.py +202 -0
  17. alf_tools/models/esm_utils/loss.py +171 -0
  18. alf_tools/models/esm_utils/scoring_function.py +129 -0
  19. alf_tools/models/esmfold.py +316 -0
  20. alf_tools/models/gp.py +955 -0
  21. alf_tools/models/mlp.py +520 -0
  22. alf_tools/models/pyrosetta.py +270 -0
  23. alf_tools/models/utils/__init__.py +33 -0
  24. alf_tools/models/utils/botorch_model_wrapper.py +326 -0
  25. alf_tools/models/utils/botorch_utils.py +340 -0
  26. alf_tools/models/utils/config_utils.py +105 -0
  27. alf_tools/models/utils/data_utils.py +81 -0
  28. alf_tools/models/utils/sequence_utils.py +134 -0
  29. alf_tools/models/utils/torch_utils.py +31 -0
  30. alf_tools/optimizer/acquisition_functions/__init__.py +35 -0
  31. alf_tools/optimizer/acquisition_functions/botorch_acquisition.py +519 -0
  32. alf_tools/optimizer/acquisition_functions/botorch_samplers.py +128 -0
  33. alf_tools/optimizer/acquisition_functions/core_set.py +127 -0
  34. alf_tools/optimizer/acquisition_functions/expected_improvement.py +64 -0
  35. alf_tools/optimizer/acquisition_functions/greedy.py +39 -0
  36. alf_tools/optimizer/acquisition_functions/thompson_sampling.py +89 -0
  37. alf_tools/optimizer/acquisition_functions/ucb.py +59 -0
  38. alf_tools/optimizer/search/__init__.py +20 -0
  39. alf_tools/optimizer/search/botorch_continuous_search.py +71 -0
  40. alf_tools/optimizer/search/single_mutant_search.py +52 -0
  41. alf_tools/utils/__init__.py +27 -0
  42. alf_tools/utils/constants.py +20 -0
  43. alf_tools-0.1.0a0.dist-info/METADATA +166 -0
  44. alf_tools-0.1.0a0.dist-info/RECORD +46 -0
  45. alf_tools-0.1.0a0.dist-info/WHEEL +5 -0
  46. alf_tools-0.1.0a0.dist-info/top_level.txt +1 -0
alf_tools/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import importlib.metadata
16
+
17
+ __version__ = importlib.metadata.version("alf-tools")
@@ -0,0 +1,28 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from alf_tools.datasets.flip import FLIP, FLIPConfig
16
+ from alf_tools.datasets.gfp import GFP
17
+ from alf_tools.datasets.proteingym import ProteinGym
18
+
19
+ try:
20
+ from alf_tools.datasets.guacamol import (
21
+ GuacaMol,
22
+ GuacaMolConfig,
23
+ GuacaMolFileInfo,
24
+ download_guacamol,
25
+ get_task_scorer,
26
+ )
27
+ except ImportError:
28
+ pass
@@ -0,0 +1,312 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import copy
16
+ import io
17
+ import logging
18
+ import zipfile
19
+ from math import floor
20
+ from pathlib import Path
21
+ from typing import Literal, Self
22
+
23
+ import numpy as np
24
+ import pandas as pd
25
+ import requests
26
+ from alf_core import BaseDataset, BaseDatasetConfig, Candidate, LabelledCandidates, ProblemType
27
+ from pydantic import model_validator
28
+
29
+ logger = logging.getLogger("alf-tools")
30
+
31
+ DATAPATH = Path(__file__).parent / "data"
32
+ FLIP_GITHUB_BASE = "https://github.com/J-SNACKKB/FLIP/raw/main/splits"
33
+
34
+ FLIP_DATASETS = Literal[
35
+ "aav",
36
+ "gb1",
37
+ "meltome",
38
+ "scl",
39
+ "sav",
40
+ ]
41
+
42
+ # Active (green) splits per dataset — orange/red splits and FASTA-only datasets excluded
43
+ FLIP_SPLITS: dict[str, list[str]] = {
44
+ "aav": ["des_mut", "mut_des", "one_vs_many", "two_vs_many", "seven_vs_many", "low_vs_high"],
45
+ "gb1": ["one_vs_rest", "two_vs_rest", "three_vs_rest", "low_vs_high"],
46
+ "meltome": ["mixed", "human", "human_cell"],
47
+ "scl": ["mixed_soft", "mixed_hard", "human_soft", "human_hard", "balanced", "mixed_vs_human_2"],
48
+ "sav": ["mixed", "human", "only_savs"],
49
+ }
50
+
51
+ # A few FLIP split names don't match their CSV file stem exactly
52
+ _SPLIT_FILE_MAP: dict[tuple[str, str], str] = {
53
+ ("meltome", "mixed"): "mixed_split",
54
+ }
55
+
56
+ _EXPECTED_COLUMNS = {"sequence", "target", "set", "validation"}
57
+
58
+
59
+ class FLIPConfig(BaseDatasetConfig):
60
+ """Configuration for FLIP benchmark datasets.
61
+
62
+ `train_ratio` controls what fraction of the FLIP train sequences form the
63
+ initial labelled training set. `validation_frac` is applied to that portion
64
+ to carve out a validation set (same as `BaseDataset`). The remaining FLIP
65
+ train sequences become the candidate pool (capped at `max_candidate_pool`).
66
+ `test_ratio` controls what fraction of the FLIP test sequences form the
67
+ evaluation set. `split_type` defaults to `"random"` and need not be set.
68
+
69
+ Attributes:
70
+ flip_dataset: Name of the FLIP dataset (e.g. `"gb1"`, `"aav"`).
71
+ flip_split: Name of the split within the dataset (e.g. `"one_vs_rest"`).
72
+ Must be an active (green-status) split. See `FLIP_SPLITS` for valid options.
73
+
74
+ Example::
75
+
76
+ config = FLIPConfig(
77
+ name="gb1_experiment",
78
+ modality="sequence",
79
+ seed=42,
80
+ flip_dataset="gb1",
81
+ flip_split="one_vs_rest",
82
+ train_ratio=0.1,
83
+ validation_frac=0.1,
84
+ test_ratio=0.8,
85
+ max_candidate_pool=5000,
86
+ )
87
+ """
88
+
89
+ flip_dataset: FLIP_DATASETS
90
+ flip_split: str
91
+ problem_type: ProblemType = ProblemType.REGRESSION
92
+
93
+ @model_validator(mode="after")
94
+ def validate_config(self) -> Self:
95
+ """Override base class validator.
96
+
97
+ train_ratio and test_ratio apply to separate FLIP pools (FLIP train and FLIP test),
98
+ so their sum is allowed to exceed 1. Also validates that flip_split is a valid
99
+ active split for the chosen flip_dataset.
100
+
101
+ Returns:
102
+ The validated configuration instance.
103
+
104
+ Raises:
105
+ ValueError: If `flip_split` is not a valid active split for `flip_dataset`.
106
+ """
107
+ if self.flip_split not in FLIP_SPLITS.get(self.flip_dataset, []):
108
+ raise ValueError(
109
+ f"'{self.flip_split}' is not a valid active split for dataset "
110
+ f"'{self.flip_dataset}'. Valid splits: {FLIP_SPLITS[self.flip_dataset]}"
111
+ )
112
+ return self
113
+
114
+
115
+ class FLIP(BaseDataset):
116
+ """FLIP benchmark dataset class.
117
+
118
+ FLIP provides pre-defined train/test splits for protein fitness landscapes.
119
+ The CSV columns are:
120
+ - sequence: amino acid string (may contain special characters)
121
+ - target: float fitness score
122
+ - set: "train" or "test"
123
+ - validation: bool, FLIP-recommended early-stopping flag (stored as a
124
+ candidate feature but not used for splitting)
125
+
126
+ The splits map to alf as follows:
127
+ - train_ratio × FLIP train -> alf train
128
+ - validation_frac × (train_ratio × FLIP train) -> alf validation
129
+ - remaining FLIP train (up to max_candidate_pool) -> alf candidate_pool
130
+ - test_ratio × FLIP test -> alf test
131
+ """
132
+
133
+ config: FLIPConfig # narrows the inherited BaseDatasetConfig type
134
+
135
+ def __init__(self, config: FLIPConfig):
136
+ """Initialise FLIP dataset."""
137
+ super().__init__(config)
138
+ self.setup()
139
+
140
+ def __repr__(self) -> str:
141
+ """Return a string representation identifying dataset and split."""
142
+ return (
143
+ f"FLIP(name={self.config.name}, modality={self.modality}, "
144
+ f"seed={self.config.seed}, "
145
+ f"flip_dataset={self.config.flip_dataset}, "
146
+ f"flip_split={self.config.flip_split})"
147
+ )
148
+
149
+ def load_dataset(self) -> LabelledCandidates:
150
+ """Load FLIP split from local cache or download from GitHub.
151
+
152
+ Downloads splits.zip for the configured dataset if not cached locally,
153
+ then extracts and parses the configured split CSV.
154
+
155
+ Returns:
156
+ LabelledCandidates with sequence data, fitness labels, and features
157
+ containing 'set' ("train"/"test") and 'validation' (bool).
158
+
159
+ Raises:
160
+ FileNotFoundError: If the split CSV is not found inside splits.zip.
161
+ requests.HTTPError: If the download from GitHub fails.
162
+ """
163
+ df = self._load_split_dataframe()
164
+
165
+ candidates = []
166
+ labels = []
167
+ for _, row in df.iterrows():
168
+ candidates.append(
169
+ Candidate(
170
+ data=row["sequence"],
171
+ modality=self.modality,
172
+ features={
173
+ "set": row["set"],
174
+ "validation": bool(row["validation"]),
175
+ },
176
+ )
177
+ )
178
+ labels.append(row["target"])
179
+
180
+ return LabelledCandidates(
181
+ candidates=candidates,
182
+ labels=np.array(labels),
183
+ )
184
+
185
+ def _split_dataset(self) -> dict[str, LabelledCandidates]:
186
+ """Split the raw dataset into train, validation, candidate_pool, and test.
187
+
188
+ The FLIP train sequences are shuffled, then partitioned using train_ratio
189
+ and validation_frac. The remainder becomes the candidate pool (capped at
190
+ max_candidate_pool). The FLIP test sequences are sampled using test_ratio.
191
+
192
+ Returns:
193
+ Dictionary with keys "train", "validation", "test", "candidate_pool".
194
+
195
+ Raises:
196
+ RuntimeError: If dataset has not been loaded yet.
197
+ """
198
+ if self._raw_dataset is None:
199
+ raise RuntimeError(
200
+ "Dataset must be loaded before splitting; "
201
+ "_raw_dataset is None — call dataset.setup() (or load_dataset()) before splitting"
202
+ )
203
+
204
+ # Separate FLIP's pre-defined train and test pools
205
+ flip_train = LabelledCandidates(candidates=[], labels=np.array([]))
206
+ flip_test = LabelledCandidates(candidates=[], labels=np.array([]))
207
+ for candidate, label in self._raw_dataset:
208
+ if candidate.features and candidate.features["set"] == "train":
209
+ flip_train.append([candidate], np.array([label]))
210
+ else:
211
+ flip_test.append([candidate], np.array([label]))
212
+
213
+ logger.debug("FLIP pools — train: %d, test: %d", len(flip_train), len(flip_test))
214
+
215
+ # Shuffle FLIP train for reproducible random splitting
216
+ flip_train = flip_train.shuffle(self.config.seed)
217
+
218
+ # Split FLIP train into train, validation, and candidate_pool
219
+ train_plus_val_size = floor(len(flip_train) * self.split_ratio["train"])
220
+ validation_size = floor(train_plus_val_size * self.split_ratio["validation_frac"])
221
+ train_size = train_plus_val_size - validation_size
222
+
223
+ train = LabelledCandidates(
224
+ candidates=flip_train.candidates[:train_size],
225
+ labels=flip_train.labels[:train_size],
226
+ )
227
+ validation = LabelledCandidates(
228
+ candidates=flip_train.candidates[train_size:train_plus_val_size],
229
+ labels=flip_train.labels[train_size:train_plus_val_size],
230
+ )
231
+
232
+ pool_candidates = flip_train.candidates[train_plus_val_size:]
233
+ pool_labels = flip_train.labels[train_plus_val_size:]
234
+ if self.config.max_candidate_pool is not None:
235
+ pool_candidates = pool_candidates[: self.config.max_candidate_pool]
236
+ pool_labels = pool_labels[: self.config.max_candidate_pool]
237
+ candidate_pool = LabelledCandidates(candidates=pool_candidates, labels=pool_labels)
238
+
239
+ # Sample FLIP test using test_ratio
240
+ test_size = floor(len(flip_test) * self.split_ratio["test"])
241
+ test = LabelledCandidates(
242
+ candidates=flip_test.candidates[:test_size],
243
+ labels=flip_test.labels[:test_size],
244
+ )
245
+
246
+ logger.debug(
247
+ "Split sizes — train: %d, validation: %d, candidate_pool: %d, test: %d",
248
+ len(train),
249
+ len(validation),
250
+ len(candidate_pool),
251
+ len(test),
252
+ )
253
+
254
+ self.init_candidate_pool = copy.deepcopy(candidate_pool)
255
+
256
+ return {
257
+ "train": train,
258
+ "validation": validation,
259
+ "test": test,
260
+ "candidate_pool": candidate_pool,
261
+ }
262
+
263
+ def _load_split_dataframe(self) -> pd.DataFrame:
264
+ """Load the split CSV as a DataFrame, downloading splits.zip if needed.
265
+
266
+ Returns:
267
+ DataFrame with columns: sequence, target, set, validation.
268
+
269
+ Raises:
270
+ FileNotFoundError: If split CSV not found inside the zip archive.
271
+ ValueError: If the loaded CSV is missing expected columns.
272
+ requests.HTTPError: If the GitHub download fails.
273
+ """
274
+ zip_path = DATAPATH / "FLIP" / self.config.flip_dataset / "splits.zip"
275
+
276
+ if not zip_path.exists():
277
+ zip_path.parent.mkdir(parents=True, exist_ok=True)
278
+ url = f"{FLIP_GITHUB_BASE}/{self.config.flip_dataset}/splits.zip"
279
+ logger.info(f"Downloading FLIP splits for '{self.config.flip_dataset}' from {url}")
280
+ response = requests.get(url, stream=True, timeout=300)
281
+ response.raise_for_status()
282
+ with open(zip_path, "wb") as f:
283
+ for chunk in response.iter_content(chunk_size=8192):
284
+ f.write(chunk)
285
+ logger.info("Download complete.")
286
+ else:
287
+ logger.debug("Using cached splits.zip at %s", zip_path)
288
+
289
+ file_stem = _SPLIT_FILE_MAP.get(
290
+ (self.config.flip_dataset, self.config.flip_split),
291
+ self.config.flip_split,
292
+ )
293
+ csv_name = f"{file_stem}.csv"
294
+ with zipfile.ZipFile(zip_path, "r") as zf:
295
+ available = zf.namelist()
296
+ match = next((n for n in available if n.endswith(csv_name)), None)
297
+ if match is None:
298
+ raise FileNotFoundError(
299
+ f"'{csv_name}' not found in splits.zip for dataset "
300
+ f"'{self.config.flip_dataset}'. Available files: {available}"
301
+ )
302
+ with zf.open(match) as csv_file:
303
+ df = pd.read_csv(io.BytesIO(csv_file.read()))
304
+
305
+ missing = _EXPECTED_COLUMNS - set(df.columns)
306
+ if missing:
307
+ raise ValueError(
308
+ f"FLIP CSV '{csv_name}' is missing expected columns: {missing}. "
309
+ f"Found: {set(df.columns)}"
310
+ )
311
+
312
+ return df
@@ -0,0 +1,73 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import logging
16
+ from pathlib import Path
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+ import requests
21
+ from alf_core import BaseDataset, BaseDatasetConfig, Candidate, LabelledCandidates
22
+
23
+ logger = logging.getLogger("alf-tools")
24
+
25
+ DATAPATH = Path(__file__).parent / "data"
26
+ FILENAME = "gfp_dataset.csv"
27
+ URL = "https://raw.githubusercontent.com/dhbrookes/CbAS/master/data/gfp_data.csv"
28
+
29
+
30
+ class GFP(BaseDataset):
31
+ """GFP dataset class."""
32
+
33
+ def __init__(self, config: BaseDatasetConfig):
34
+ """Initialize the GFP dataset.
35
+
36
+ Args:
37
+ config: Configuration for the GFP dataset.
38
+ """
39
+ super().__init__(config)
40
+ self.setup()
41
+
42
+ def load_dataset(self) -> LabelledCandidates:
43
+ """Load GFP dataset from local file or download from URL if not present.
44
+ Clean dataset and return as HF dataset.
45
+
46
+ Returns:
47
+ A LabelledCandidates object containing the GFP data.
48
+
49
+ Raises:
50
+ FileNotFoundError: If the GFP dataset file is not found.
51
+ """
52
+ filepath = DATAPATH / FILENAME
53
+ if not filepath.exists():
54
+ DATAPATH.mkdir(parents=True, exist_ok=True)
55
+ response = requests.get(URL)
56
+ if response.status_code == 200:
57
+ with open(filepath, "wb") as file:
58
+ file.write(response.content)
59
+ logger.info("GFP dataset downloaded successfully.")
60
+ else:
61
+ raise FileNotFoundError(
62
+ f"Failed to download GFP dataset. Status code: {response.status_code}"
63
+ )
64
+
65
+ gfp_dataset = pd.read_csv(filepath)
66
+ # Note, here we are only using the first 1000 rows of the dataset,
67
+ # otherwise the candidate pool is too large and becomes compute intensive
68
+ data = list(gfp_dataset["nucSequence"])[:1000]
69
+ labels = np.array(gfp_dataset["medianBrightness"].values)[:1000]
70
+ return LabelledCandidates(
71
+ candidates=[Candidate(data=data, modality=self.modality) for data in data],
72
+ labels=labels,
73
+ )
@@ -0,0 +1,17 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from alf_tools.datasets.guacamol.guacamol_dataset import GuacaMol, GuacaMolConfig
16
+ from alf_tools.datasets.guacamol.guacamol_scoring import get_task_scorer
17
+ from alf_tools.datasets.guacamol.guacamol_utils import GuacaMolFileInfo, download_guacamol