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.
Files changed (42) hide show
  1. alf_core-0.1.0a0/PKG-INFO +119 -0
  2. alf_core-0.1.0a0/README.md +92 -0
  3. alf_core-0.1.0a0/alf_core/__init__.py +58 -0
  4. alf_core-0.1.0a0/alf_core/dataclasses/__init__.py +26 -0
  5. alf_core-0.1.0a0/alf_core/dataclasses/candidate.py +225 -0
  6. alf_core-0.1.0a0/alf_core/dataclasses/labelled_candidates.py +234 -0
  7. alf_core-0.1.0a0/alf_core/dataclasses/predictions.py +119 -0
  8. alf_core-0.1.0a0/alf_core/dataclasses/results.py +85 -0
  9. alf_core-0.1.0a0/alf_core/dataclasses/round_metrics.py +42 -0
  10. alf_core-0.1.0a0/alf_core/dataclasses/state.py +72 -0
  11. alf_core-0.1.0a0/alf_core/dataclasses/surrogate_epoch_metrics.py +55 -0
  12. alf_core-0.1.0a0/alf_core/dataset/base_dataset.py +381 -0
  13. alf_core-0.1.0a0/alf_core/dataset/splitting_utils.py +268 -0
  14. alf_core-0.1.0a0/alf_core/model/base_model.py +154 -0
  15. alf_core-0.1.0a0/alf_core/model/normaliser.py +342 -0
  16. alf_core-0.1.0a0/alf_core/optimizer/acquisition_function.py +39 -0
  17. alf_core-0.1.0a0/alf_core/optimizer/optimizer.py +126 -0
  18. alf_core-0.1.0a0/alf_core/optimizer/search.py +199 -0
  19. alf_core-0.1.0a0/alf_core/oracle/oracle.py +74 -0
  20. alf_core-0.1.0a0/alf_core/surrogate/surrogate.py +95 -0
  21. alf_core-0.1.0a0/alf_core/tasks/base_task.py +119 -0
  22. alf_core-0.1.0a0/alf_core/tasks/design_task.py +122 -0
  23. alf_core-0.1.0a0/alf_core/tasks/supervised_task.py +74 -0
  24. alf_core-0.1.0a0/alf_core/tasks/zeroshot_task.py +67 -0
  25. alf_core-0.1.0a0/alf_core/utils/enums.py +29 -0
  26. alf_core-0.1.0a0/alf_core/utils/metrics/__init__.py +73 -0
  27. alf_core-0.1.0a0/alf_core/utils/metrics/acquisition_batch.py +215 -0
  28. alf_core-0.1.0a0/alf_core/utils/metrics/aggregate.py +182 -0
  29. alf_core-0.1.0a0/alf_core/utils/metrics/base.py +176 -0
  30. alf_core-0.1.0a0/alf_core/utils/metrics/calibration.py +93 -0
  31. alf_core-0.1.0a0/alf_core/utils/metrics/classification.py +185 -0
  32. alf_core-0.1.0a0/alf_core/utils/metrics/regression.py +704 -0
  33. alf_core-0.1.0a0/alf_core/utils/state_logger.py +311 -0
  34. alf_core-0.1.0a0/alf_core.egg-info/PKG-INFO +119 -0
  35. alf_core-0.1.0a0/alf_core.egg-info/SOURCES.txt +40 -0
  36. alf_core-0.1.0a0/alf_core.egg-info/dependency_links.txt +1 -0
  37. alf_core-0.1.0a0/alf_core.egg-info/requires.txt +7 -0
  38. alf_core-0.1.0a0/alf_core.egg-info/top_level.txt +1 -0
  39. alf_core-0.1.0a0/pyproject.toml +46 -0
  40. alf_core-0.1.0a0/setup.cfg +4 -0
  41. alf_core-0.1.0a0/tests/test_oracle.py +114 -0
  42. 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
+ [![PyPI](https://img.shields.io/pypi/v/alf-core.svg)](https://pypi.org/project/alf-core/)
31
+ [![Python Version](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
32
+ [![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](https://github.com/instadeepai/alf/blob/main/LICENSE)
33
+ [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/surana01/114eb5680493468e40f5a528c08f1888/raw/alf-core-coverage.json)](https://github.com/instadeepai/alf/tree/main/core)
34
+ [![Docs](https://img.shields.io/badge/docs-instadeepai.github.io%2Falf-blue)](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
+ [![PyPI](https://img.shields.io/pypi/v/alf-core.svg)](https://pypi.org/project/alf-core/)
4
+ [![Python Version](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
5
+ [![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](https://github.com/instadeepai/alf/blob/main/LICENSE)
6
+ [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/surana01/114eb5680493468e40f5a528c08f1888/raw/alf-core-coverage.json)](https://github.com/instadeepai/alf/tree/main/core)
7
+ [![Docs](https://img.shields.io/badge/docs-instadeepai.github.io%2Falf-blue)](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
+ )