alf_core 0.1.0__tar.gz

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