alf_core 0.1.0a0__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.
- alf_core-0.1.0a0/PKG-INFO +119 -0
- alf_core-0.1.0a0/README.md +92 -0
- alf_core-0.1.0a0/alf_core/__init__.py +58 -0
- alf_core-0.1.0a0/alf_core/dataclasses/__init__.py +26 -0
- alf_core-0.1.0a0/alf_core/dataclasses/candidate.py +225 -0
- alf_core-0.1.0a0/alf_core/dataclasses/labelled_candidates.py +234 -0
- alf_core-0.1.0a0/alf_core/dataclasses/predictions.py +119 -0
- alf_core-0.1.0a0/alf_core/dataclasses/results.py +85 -0
- alf_core-0.1.0a0/alf_core/dataclasses/round_metrics.py +42 -0
- alf_core-0.1.0a0/alf_core/dataclasses/state.py +72 -0
- alf_core-0.1.0a0/alf_core/dataclasses/surrogate_epoch_metrics.py +55 -0
- alf_core-0.1.0a0/alf_core/dataset/base_dataset.py +381 -0
- alf_core-0.1.0a0/alf_core/dataset/splitting_utils.py +268 -0
- alf_core-0.1.0a0/alf_core/model/base_model.py +154 -0
- alf_core-0.1.0a0/alf_core/model/normaliser.py +342 -0
- alf_core-0.1.0a0/alf_core/optimizer/acquisition_function.py +39 -0
- alf_core-0.1.0a0/alf_core/optimizer/optimizer.py +126 -0
- alf_core-0.1.0a0/alf_core/optimizer/search.py +199 -0
- alf_core-0.1.0a0/alf_core/oracle/oracle.py +74 -0
- alf_core-0.1.0a0/alf_core/surrogate/surrogate.py +95 -0
- alf_core-0.1.0a0/alf_core/tasks/base_task.py +119 -0
- alf_core-0.1.0a0/alf_core/tasks/design_task.py +122 -0
- alf_core-0.1.0a0/alf_core/tasks/supervised_task.py +74 -0
- alf_core-0.1.0a0/alf_core/tasks/zeroshot_task.py +67 -0
- alf_core-0.1.0a0/alf_core/utils/enums.py +29 -0
- alf_core-0.1.0a0/alf_core/utils/metrics/__init__.py +73 -0
- alf_core-0.1.0a0/alf_core/utils/metrics/acquisition_batch.py +215 -0
- alf_core-0.1.0a0/alf_core/utils/metrics/aggregate.py +182 -0
- alf_core-0.1.0a0/alf_core/utils/metrics/base.py +176 -0
- alf_core-0.1.0a0/alf_core/utils/metrics/calibration.py +93 -0
- alf_core-0.1.0a0/alf_core/utils/metrics/classification.py +185 -0
- alf_core-0.1.0a0/alf_core/utils/metrics/regression.py +704 -0
- alf_core-0.1.0a0/alf_core/utils/state_logger.py +311 -0
- alf_core-0.1.0a0/alf_core.egg-info/PKG-INFO +119 -0
- alf_core-0.1.0a0/alf_core.egg-info/SOURCES.txt +40 -0
- alf_core-0.1.0a0/alf_core.egg-info/dependency_links.txt +1 -0
- alf_core-0.1.0a0/alf_core.egg-info/requires.txt +7 -0
- alf_core-0.1.0a0/alf_core.egg-info/top_level.txt +1 -0
- alf_core-0.1.0a0/pyproject.toml +46 -0
- alf_core-0.1.0a0/setup.cfg +4 -0
- alf_core-0.1.0a0/tests/test_oracle.py +114 -0
- alf_core-0.1.0a0/tests/test_surrogate_featurise.py +118 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alf_core
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: A Python package for performing active learning experiments.
|
|
5
|
+
Author: InstaDeep Ltd
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://instadeepai.github.io/alf/
|
|
8
|
+
Project-URL: Repository, https://github.com/instadeepai/alf
|
|
9
|
+
Project-URL: Documentation, https://instadeepai.github.io/alf/
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/instadeepai/alf/issues
|
|
11
|
+
Keywords: active learning,bayesian optimization,machine learning,scientific discovery
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: <3.14.1,>=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: beartype>=0.18.0
|
|
21
|
+
Requires-Dist: jaxtyping>=0.3.5
|
|
22
|
+
Requires-Dist: numpy>=1.21.0
|
|
23
|
+
Requires-Dist: pandas>=1.3.0
|
|
24
|
+
Requires-Dist: pydantic
|
|
25
|
+
Requires-Dist: scipy>=1.9.0
|
|
26
|
+
Requires-Dist: scikit-learn>=1.0.0
|
|
27
|
+
|
|
28
|
+
# alf-core
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/alf-core/)
|
|
31
|
+
[](https://www.python.org/downloads/)
|
|
32
|
+
[](https://github.com/instadeepai/alf/blob/main/LICENSE)
|
|
33
|
+
[](https://github.com/instadeepai/alf/tree/main/core)
|
|
34
|
+
[](https://instadeepai.github.io/alf/)
|
|
35
|
+
|
|
36
|
+
**The lightweight, dependency-minimal foundation of ALF (Active Learning Framework).**
|
|
37
|
+
|
|
38
|
+
`alf-core` provides the base classes, core data structures, and the active-learning loop
|
|
39
|
+
for iterative optimisation in computational science — optimising high-dimensional,
|
|
40
|
+
combinatorially vast search spaces where each label is expensive (wet-lab assays,
|
|
41
|
+
simulations, measurements). It ships with no ML-framework dependencies (only numpy,
|
|
42
|
+
pandas, scipy), so it is standalone and domain-agnostic. For ready-to-use models,
|
|
43
|
+
datasets, and acquisition functions, install
|
|
44
|
+
[alf-tools](https://github.com/instadeepai/alf/blob/main/tools/README.md).
|
|
45
|
+
|
|
46
|
+
<div align="center">
|
|
47
|
+
<img src="https://raw.githubusercontent.com/instadeepai/alf/main/docs/imgs/alf_components.svg" alt="ALF Components" width="70%">
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install alf-core
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
`alf-core` is the framework layer: you supply your own `BaseDataset` and `BaseModel`
|
|
59
|
+
subclasses (or install [alf-tools](https://github.com/instadeepai/alf/blob/main/tools/README.md)
|
|
60
|
+
for ready-made ones), then wire them into the active-learning loop.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from alf_core import (
|
|
64
|
+
DatasetSearch,
|
|
65
|
+
DesignTask,
|
|
66
|
+
Optimizer,
|
|
67
|
+
Oracle,
|
|
68
|
+
Surrogate,
|
|
69
|
+
TerminalStateLogger,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Bring your own BaseDataset, BaseModel, and AcquisitionFunction subclasses
|
|
73
|
+
dataset = MyDataset(...)
|
|
74
|
+
surrogate = Surrogate(model=MyModel())
|
|
75
|
+
optimizer = Optimizer(acquisition_fn=MyAcquisition(), search_fn=DatasetSearch())
|
|
76
|
+
oracle = Oracle(scorer=dataset)
|
|
77
|
+
|
|
78
|
+
# Run the active-learning loop for 5 rounds, acquiring 100 candidates per round
|
|
79
|
+
task = DesignTask(num_acq_rounds=5, acq_batch_size=100)
|
|
80
|
+
state = task.setup(dataset=dataset, surrogate=surrogate)
|
|
81
|
+
task.run(
|
|
82
|
+
state=state,
|
|
83
|
+
state_loggers=[TerminalStateLogger()],
|
|
84
|
+
optimizer=optimizer,
|
|
85
|
+
oracle=oracle,
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For a complete, runnable `alf-core`-only example (a bootstrap-ensemble surrogate and a
|
|
90
|
+
Probability of Improvement acquisition function built from scratch with numpy/scipy), see the
|
|
91
|
+
[ALF Core Quickstart notebook](https://github.com/instadeepai/alf/blob/main/tutorials/alf_core_quickstart.ipynb).
|
|
92
|
+
|
|
93
|
+
## Key concepts
|
|
94
|
+
|
|
95
|
+
ALF runs the active-learning loop over a small set of swappable components:
|
|
96
|
+
|
|
97
|
+
- **Dataset** (`BaseDataset`) — loads, splits, and queries candidate data
|
|
98
|
+
- **Model** (`BaseModel`) — the surrogate/oracle/generator backbone you implement
|
|
99
|
+
- **Surrogate** (`Surrogate`) — wraps a model to predict fitness and uncertainty
|
|
100
|
+
- **Oracle** (`Oracle`) — returns ground-truth labels (offline pool or live scorer)
|
|
101
|
+
- **Optimizer** (`Optimizer`) — proposes the next batch via acquisition + search
|
|
102
|
+
- **Acquisition function** (`AcquisitionFunction`) — scores candidates to acquire
|
|
103
|
+
- **Search strategy** (`BaseSearch`) — defines the candidate pool to score
|
|
104
|
+
- **State** (`State`) — tracks rounds, history, and metrics across the loop
|
|
105
|
+
- **Tasks** (`DesignTask`, `SupervisedTask`, `ZeroShotTask`) — drive the multi-round
|
|
106
|
+
loop, fixed-data training, or no-train evaluation
|
|
107
|
+
|
|
108
|
+
## Documentation
|
|
109
|
+
|
|
110
|
+
- **Core concepts:** [how the components fit together](https://instadeepai.github.io/alf/explanation/core-concepts.html)
|
|
111
|
+
- **API reference:** [every class and method](https://instadeepai.github.io/alf/api/alf_core/index.html)
|
|
112
|
+
- **Glossary:** [terms and benchmark metrics](https://instadeepai.github.io/alf/reference/glossary.html)
|
|
113
|
+
- **Tutorials:** [tutorials/](https://github.com/instadeepai/alf/tree/main/tutorials)
|
|
114
|
+
- **Full documentation:** [instadeepai.github.io/alf](https://instadeepai.github.io/alf/)
|
|
115
|
+
- **Ready-to-use tools:** [alf-tools](https://github.com/instadeepai/alf/blob/main/tools/README.md)
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
Apache License 2.0 — see [LICENSE](https://github.com/instadeepai/alf/blob/main/LICENSE).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# alf-core
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/alf-core/)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](https://github.com/instadeepai/alf/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/instadeepai/alf/tree/main/core)
|
|
7
|
+
[](https://instadeepai.github.io/alf/)
|
|
8
|
+
|
|
9
|
+
**The lightweight, dependency-minimal foundation of ALF (Active Learning Framework).**
|
|
10
|
+
|
|
11
|
+
`alf-core` provides the base classes, core data structures, and the active-learning loop
|
|
12
|
+
for iterative optimisation in computational science — optimising high-dimensional,
|
|
13
|
+
combinatorially vast search spaces where each label is expensive (wet-lab assays,
|
|
14
|
+
simulations, measurements). It ships with no ML-framework dependencies (only numpy,
|
|
15
|
+
pandas, scipy), so it is standalone and domain-agnostic. For ready-to-use models,
|
|
16
|
+
datasets, and acquisition functions, install
|
|
17
|
+
[alf-tools](https://github.com/instadeepai/alf/blob/main/tools/README.md).
|
|
18
|
+
|
|
19
|
+
<div align="center">
|
|
20
|
+
<img src="https://raw.githubusercontent.com/instadeepai/alf/main/docs/imgs/alf_components.svg" alt="ALF Components" width="70%">
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install alf-core
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
`alf-core` is the framework layer: you supply your own `BaseDataset` and `BaseModel`
|
|
32
|
+
subclasses (or install [alf-tools](https://github.com/instadeepai/alf/blob/main/tools/README.md)
|
|
33
|
+
for ready-made ones), then wire them into the active-learning loop.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from alf_core import (
|
|
37
|
+
DatasetSearch,
|
|
38
|
+
DesignTask,
|
|
39
|
+
Optimizer,
|
|
40
|
+
Oracle,
|
|
41
|
+
Surrogate,
|
|
42
|
+
TerminalStateLogger,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Bring your own BaseDataset, BaseModel, and AcquisitionFunction subclasses
|
|
46
|
+
dataset = MyDataset(...)
|
|
47
|
+
surrogate = Surrogate(model=MyModel())
|
|
48
|
+
optimizer = Optimizer(acquisition_fn=MyAcquisition(), search_fn=DatasetSearch())
|
|
49
|
+
oracle = Oracle(scorer=dataset)
|
|
50
|
+
|
|
51
|
+
# Run the active-learning loop for 5 rounds, acquiring 100 candidates per round
|
|
52
|
+
task = DesignTask(num_acq_rounds=5, acq_batch_size=100)
|
|
53
|
+
state = task.setup(dataset=dataset, surrogate=surrogate)
|
|
54
|
+
task.run(
|
|
55
|
+
state=state,
|
|
56
|
+
state_loggers=[TerminalStateLogger()],
|
|
57
|
+
optimizer=optimizer,
|
|
58
|
+
oracle=oracle,
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For a complete, runnable `alf-core`-only example (a bootstrap-ensemble surrogate and a
|
|
63
|
+
Probability of Improvement acquisition function built from scratch with numpy/scipy), see the
|
|
64
|
+
[ALF Core Quickstart notebook](https://github.com/instadeepai/alf/blob/main/tutorials/alf_core_quickstart.ipynb).
|
|
65
|
+
|
|
66
|
+
## Key concepts
|
|
67
|
+
|
|
68
|
+
ALF runs the active-learning loop over a small set of swappable components:
|
|
69
|
+
|
|
70
|
+
- **Dataset** (`BaseDataset`) — loads, splits, and queries candidate data
|
|
71
|
+
- **Model** (`BaseModel`) — the surrogate/oracle/generator backbone you implement
|
|
72
|
+
- **Surrogate** (`Surrogate`) — wraps a model to predict fitness and uncertainty
|
|
73
|
+
- **Oracle** (`Oracle`) — returns ground-truth labels (offline pool or live scorer)
|
|
74
|
+
- **Optimizer** (`Optimizer`) — proposes the next batch via acquisition + search
|
|
75
|
+
- **Acquisition function** (`AcquisitionFunction`) — scores candidates to acquire
|
|
76
|
+
- **Search strategy** (`BaseSearch`) — defines the candidate pool to score
|
|
77
|
+
- **State** (`State`) — tracks rounds, history, and metrics across the loop
|
|
78
|
+
- **Tasks** (`DesignTask`, `SupervisedTask`, `ZeroShotTask`) — drive the multi-round
|
|
79
|
+
loop, fixed-data training, or no-train evaluation
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
- **Core concepts:** [how the components fit together](https://instadeepai.github.io/alf/explanation/core-concepts.html)
|
|
84
|
+
- **API reference:** [every class and method](https://instadeepai.github.io/alf/api/alf_core/index.html)
|
|
85
|
+
- **Glossary:** [terms and benchmark metrics](https://instadeepai.github.io/alf/reference/glossary.html)
|
|
86
|
+
- **Tutorials:** [tutorials/](https://github.com/instadeepai/alf/tree/main/tutorials)
|
|
87
|
+
- **Full documentation:** [instadeepai.github.io/alf](https://instadeepai.github.io/alf/)
|
|
88
|
+
- **Ready-to-use tools:** [alf-tools](https://github.com/instadeepai/alf/blob/main/tools/README.md)
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
Apache License 2.0 — see [LICENSE](https://github.com/instadeepai/alf/blob/main/LICENSE).
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
# This file makes alf_core a Python package
|
|
16
|
+
import importlib.metadata
|
|
17
|
+
|
|
18
|
+
from alf_core.dataclasses import (
|
|
19
|
+
Candidate,
|
|
20
|
+
LabelledCandidates,
|
|
21
|
+
Modality,
|
|
22
|
+
Predictions,
|
|
23
|
+
Results,
|
|
24
|
+
State,
|
|
25
|
+
SurrogateEpochMetrics,
|
|
26
|
+
)
|
|
27
|
+
from alf_core.dataset.base_dataset import BaseDataset, BaseDatasetConfig
|
|
28
|
+
from alf_core.model.base_model import BaseModel, BaseTrainConfig
|
|
29
|
+
from alf_core.model.normaliser import (
|
|
30
|
+
InputNormaliser,
|
|
31
|
+
InputStandardiser,
|
|
32
|
+
OutputStandardiser,
|
|
33
|
+
make_input_transform,
|
|
34
|
+
)
|
|
35
|
+
from alf_core.optimizer.acquisition_function import AcquisitionFunction
|
|
36
|
+
from alf_core.optimizer.optimizer import Optimizer
|
|
37
|
+
from alf_core.optimizer.search import (
|
|
38
|
+
BaseSearch,
|
|
39
|
+
DatasetSearch,
|
|
40
|
+
GeneratorSearch,
|
|
41
|
+
ModelProtocolSearch,
|
|
42
|
+
ProtocolSearch,
|
|
43
|
+
SearchProtocol,
|
|
44
|
+
)
|
|
45
|
+
from alf_core.oracle.oracle import Oracle
|
|
46
|
+
from alf_core.surrogate.surrogate import Surrogate
|
|
47
|
+
from alf_core.tasks.base_task import BaseTask
|
|
48
|
+
from alf_core.tasks.design_task import DesignTask
|
|
49
|
+
from alf_core.tasks.supervised_task import SupervisedTask
|
|
50
|
+
from alf_core.tasks.zeroshot_task import ZeroShotTask
|
|
51
|
+
from alf_core.utils.enums import ProblemType
|
|
52
|
+
from alf_core.utils.state_logger import (
|
|
53
|
+
FileStateLogger,
|
|
54
|
+
StateLogger,
|
|
55
|
+
TerminalStateLogger,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__version__ = importlib.metadata.version("alf-core")
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
# ruff: noqa: E402
|
|
16
|
+
import beartype.claw
|
|
17
|
+
|
|
18
|
+
beartype.claw.beartype_this_package()
|
|
19
|
+
|
|
20
|
+
from alf_core.dataclasses.candidate import Candidate, Modality
|
|
21
|
+
from alf_core.dataclasses.labelled_candidates import LabelledCandidates
|
|
22
|
+
from alf_core.dataclasses.predictions import Predictions
|
|
23
|
+
from alf_core.dataclasses.results import Results
|
|
24
|
+
from alf_core.dataclasses.round_metrics import RoundMetrics
|
|
25
|
+
from alf_core.dataclasses.state import State
|
|
26
|
+
from alf_core.dataclasses.surrogate_epoch_metrics import SurrogateEpochMetrics
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import TYPE_CHECKING, Any, TypeAlias, Union
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
DataFrameCompatible: TypeAlias = Union[
|
|
26
|
+
str, int, float, bool, dict, list, tuple, np.ndarray, torch.Tensor
|
|
27
|
+
]
|
|
28
|
+
else:
|
|
29
|
+
DataFrameCompatible: TypeAlias = Union[
|
|
30
|
+
str, int, float, bool, dict, list, tuple, np.ndarray, Any
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
import torch
|
|
35
|
+
|
|
36
|
+
HAS_TORCH = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
HAS_TORCH = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Modality(Enum):
|
|
42
|
+
"""The *kind* of candidate — used to match datasets with compatible models and metrics.
|
|
43
|
+
|
|
44
|
+
It is the data's domain where one exists, not how the data is stored. This is why a
|
|
45
|
+
protein sequence and a SMILES string are distinct modalities (``SEQUENCE`` vs
|
|
46
|
+
``MOLECULE``) even though both are stored as ``str``: they pair with different models
|
|
47
|
+
and metrics. Storage type is never encoded here — it is inferred from ``type(data)``
|
|
48
|
+
(see :meth:`Candidate.to_serializable`).
|
|
49
|
+
|
|
50
|
+
Members:
|
|
51
|
+
SEQUENCE: Biological sequences (protein / nucleotide), as strings.
|
|
52
|
+
MOLECULE: Small molecules, as SMILES strings.
|
|
53
|
+
TABULAR: Domain-agnostic numeric feature vectors (arrays, tensors, scalars, dicts).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
SEQUENCE = "sequence"
|
|
57
|
+
MOLECULE = "molecule"
|
|
58
|
+
TABULAR = "tabular"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(eq=False, unsafe_hash=False)
|
|
62
|
+
class Candidate:
|
|
63
|
+
"""A candidate is a data point with a modality and features.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
data: The raw data of the candidate (e.g., a sequence string, a SMILES string,
|
|
67
|
+
a feature vector).
|
|
68
|
+
modality: The data's domain (see :class:`Modality`) — what it represents, e.g.
|
|
69
|
+
``"sequence"`` or ``"molecule"``. Not its storage type.
|
|
70
|
+
features: Optional dictionary of precomputed features for the candidate.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
data: Any
|
|
74
|
+
modality: Modality | str
|
|
75
|
+
features: dict | None = None
|
|
76
|
+
|
|
77
|
+
def __post_init__(self) -> None:
|
|
78
|
+
"""Check and convert modality to Modality enum if necessary and
|
|
79
|
+
initialize features to empty dict if None.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If the modality is not a valid Modality enum.
|
|
83
|
+
"""
|
|
84
|
+
if not isinstance(self.modality, Modality):
|
|
85
|
+
try:
|
|
86
|
+
self.modality = Modality(self.modality)
|
|
87
|
+
except ValueError:
|
|
88
|
+
raise ValueError(f"Invalid modality: {self.modality}")
|
|
89
|
+
|
|
90
|
+
if self.features is None:
|
|
91
|
+
self.features = {}
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
"""Return a string representation of the candidate.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A string representation showing the candidate's data, modality, and features.
|
|
98
|
+
"""
|
|
99
|
+
return f"Candidate(data={self.data}, modality={self.modality}, features={self.features})"
|
|
100
|
+
|
|
101
|
+
def _safe_equal(self, a: Any, b: Any) -> bool:
|
|
102
|
+
"""Compare two values, handling numpy arrays and nested structures.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
a: First value to compare.
|
|
106
|
+
b: Second value to compare.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if the values are equal, False otherwise.
|
|
110
|
+
"""
|
|
111
|
+
# Handle None cases
|
|
112
|
+
if a is None and b is None:
|
|
113
|
+
return True
|
|
114
|
+
if a is None or b is None:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
# Handle numpy arrays
|
|
118
|
+
if isinstance(a, np.ndarray) and isinstance(b, np.ndarray):
|
|
119
|
+
return np.array_equal(a, b, equal_nan=True)
|
|
120
|
+
|
|
121
|
+
# Handle torch tensors
|
|
122
|
+
if HAS_TORCH and isinstance(a, torch.Tensor) and isinstance(b, torch.Tensor):
|
|
123
|
+
return torch.equal(a, b)
|
|
124
|
+
|
|
125
|
+
# Handle dict (for features)
|
|
126
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
|
127
|
+
if a.keys() != b.keys():
|
|
128
|
+
return False
|
|
129
|
+
return all(self._safe_equal(a[k], b[k]) for k in a.keys())
|
|
130
|
+
|
|
131
|
+
# Handle lists/tuples (for nested data)
|
|
132
|
+
if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
|
|
133
|
+
if len(a) != len(b):
|
|
134
|
+
return False
|
|
135
|
+
return all(self._safe_equal(x, y) for x, y in zip(a, b))
|
|
136
|
+
|
|
137
|
+
# Default comparison
|
|
138
|
+
# Note, if comparison fails, an error will be thrown
|
|
139
|
+
result = a == b
|
|
140
|
+
# Handle case where comparison returns array-like object
|
|
141
|
+
# Convert to boolean if possible
|
|
142
|
+
if hasattr(result, "__len__") and len(result) == 1:
|
|
143
|
+
return bool(result[0])
|
|
144
|
+
elif hasattr(result, "item"): # For single-element tensors/arrays
|
|
145
|
+
return bool(result.item())
|
|
146
|
+
return bool(result)
|
|
147
|
+
|
|
148
|
+
def __eq__(self, other: object) -> bool:
|
|
149
|
+
"""Compare two Candidate objects for equality.
|
|
150
|
+
|
|
151
|
+
Handles numpy arrays in data and features fields correctly.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
other: The object to compare with.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
True if the candidates are equal, False otherwise.
|
|
158
|
+
"""
|
|
159
|
+
if not isinstance(other, Candidate):
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
self._safe_equal(self.data, other.data)
|
|
164
|
+
and self.modality == other.modality
|
|
165
|
+
and self._safe_equal(self.features, other.features)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
__hash__ = None # type: ignore[assignment]
|
|
169
|
+
|
|
170
|
+
def to_serializable(self) -> DataFrameCompatible | None:
|
|
171
|
+
"""Convert candidate data to a format suitable for pandas DataFrame storage.
|
|
172
|
+
|
|
173
|
+
Dispatch is based on the **type** of ``data``, not on :attr:`modality`. Modality
|
|
174
|
+
describes the data's domain (see :class:`Modality`); how it is stored — and
|
|
175
|
+
therefore how it is serialised — is determined by its Python type:
|
|
176
|
+
|
|
177
|
+
- ``str`` (sequences, SMILES, JSON-encoded payloads): returned unchanged.
|
|
178
|
+
- ``torch.Tensor``: converted to a numpy array for compact storage.
|
|
179
|
+
- numpy array, scalar (Python ``int``/``float``/``bool`` or a numpy scalar such
|
|
180
|
+
as ``np.int64``), ``dict``, ``list``, ``tuple``, pandas ``Series``: returned
|
|
181
|
+
unchanged.
|
|
182
|
+
- ``None``: returned as ``None``.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
DataFrameCompatible: The candidate data in a DataFrame-compatible format.
|
|
186
|
+
Common types include str, dict, np.ndarray, pd.Series, or torch.Tensor.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
TypeError: If the data type is not DataFrame-compatible.
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
>>> # A sequence or SMILES string is stored as-is
|
|
193
|
+
>>> Candidate(data="ACDEFG", modality=Modality.SEQUENCE).to_serializable()
|
|
194
|
+
'ACDEFG'
|
|
195
|
+
|
|
196
|
+
>>> Candidate(data="CC(=O)O", modality=Modality.MOLECULE).to_serializable()
|
|
197
|
+
'CC(=O)O'
|
|
198
|
+
|
|
199
|
+
>>> # A feature dict is stored as-is
|
|
200
|
+
>>> c = Candidate(data={"age": 32, "height": 178}, modality=Modality.TABULAR)
|
|
201
|
+
>>> c.to_serializable()
|
|
202
|
+
{'age': 32, 'height': 178}
|
|
203
|
+
"""
|
|
204
|
+
data = self.data
|
|
205
|
+
if data is None:
|
|
206
|
+
return None
|
|
207
|
+
# Strings (sequences, SMILES, JSON-encoded payloads) are stored as-is.
|
|
208
|
+
if isinstance(data, str):
|
|
209
|
+
return data
|
|
210
|
+
# Torch tensors are converted to numpy arrays for compact storage.
|
|
211
|
+
if HAS_TORCH and isinstance(data, torch.Tensor):
|
|
212
|
+
return data.cpu().numpy()
|
|
213
|
+
# numpy arrays, scalars (incl. numpy scalars like np.int64), and standard
|
|
214
|
+
# containers are stored as-is.
|
|
215
|
+
if isinstance(data, (int, float, bool, np.generic, dict, np.ndarray, list, tuple)):
|
|
216
|
+
return data
|
|
217
|
+
# pandas Series (checked without importing pandas).
|
|
218
|
+
if data.__class__.__name__ == "Series":
|
|
219
|
+
return data
|
|
220
|
+
|
|
221
|
+
raise TypeError(
|
|
222
|
+
f"Cannot serialise candidate data of type {type(data).__name__}. Supported "
|
|
223
|
+
f"types: str, int, float, bool, dict, list, tuple, numpy.ndarray, "
|
|
224
|
+
f"pandas.Series, or torch.Tensor."
|
|
225
|
+
)
|