autoencodix 0.2.0__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.
Potentially problematic release.
This version of autoencodix might be problematic. Click here for more details.
- autoencodix/__init__.py +27 -0
- autoencodix/base/.ipynb_checkpoints/_base_pipeline-checkpoint.py +1274 -0
- autoencodix/base/.ipynb_checkpoints/_base_trainer-checkpoint.py +247 -0
- autoencodix/base/__init__.py +21 -0
- autoencodix/base/_base_autoencoder.py +138 -0
- autoencodix/base/_base_dataset.py +103 -0
- autoencodix/base/_base_evaluator.py +34 -0
- autoencodix/base/_base_loss.py +311 -0
- autoencodix/base/_base_pipeline.py +1355 -0
- autoencodix/base/_base_predictor.py +6 -0
- autoencodix/base/_base_preprocessor.py +1033 -0
- autoencodix/base/_base_trainer.py +259 -0
- autoencodix/base/_base_visualizer.py +578 -0
- autoencodix/base/sampleconfig.yaml +132 -0
- autoencodix/configs/__init__.py +21 -0
- autoencodix/configs/default_config.py +652 -0
- autoencodix/configs/disentanglix_config.py +15 -0
- autoencodix/configs/maskix_config.py +39 -0
- autoencodix/configs/ontix_config.py +35 -0
- autoencodix/configs/stackix_config.py +33 -0
- autoencodix/configs/vanillix_config.py +18 -0
- autoencodix/configs/varix_config.py +15 -0
- autoencodix/configs/xmodalix_config.py +43 -0
- autoencodix/disentanglix.py +72 -0
- autoencodix/evaluate/.ipynb_checkpoints/evaluate-checkpoint.py +532 -0
- autoencodix/evaluate/__init__.py +4 -0
- autoencodix/evaluate/_general_evaluator.py +730 -0
- autoencodix/evaluate/_xmodalix_evaluator.py +376 -0
- autoencodix/imagix.py +76 -0
- autoencodix/losses/disentanglix_loss.py +215 -0
- autoencodix/losses/maskix_loss.py +94 -0
- autoencodix/losses/vanillix_loss.py +28 -0
- autoencodix/losses/varix_loss.py +102 -0
- autoencodix/losses/xmodal_loss.py +304 -0
- autoencodix/maskix.py +108 -0
- autoencodix/modeling/.ipynb_checkpoints/__init__-checkpoint.py +16 -0
- autoencodix/modeling/.ipynb_checkpoints/_captum_forward-checkpoint.py +16 -0
- autoencodix/modeling/__init__.py +20 -0
- autoencodix/modeling/_captum_forward.py +20 -0
- autoencodix/modeling/_classifier.py +45 -0
- autoencodix/modeling/_imagevae_architecture.py +296 -0
- autoencodix/modeling/_imgfast_architecture.py +288 -0
- autoencodix/modeling/_layer_factory.py +71 -0
- autoencodix/modeling/_maskix_architecture.py +190 -0
- autoencodix/modeling/_ontix_architecture.py +305 -0
- autoencodix/modeling/_vanillix_architecture.py +136 -0
- autoencodix/modeling/_varix_architecture.py +192 -0
- autoencodix/notebooks/__init__.py +0 -0
- autoencodix/ontix.py +136 -0
- autoencodix/py.typed +0 -0
- autoencodix/stackix.py +122 -0
- autoencodix/trainers/__init__.py +6 -0
- autoencodix/trainers/_general_trainer.py +525 -0
- autoencodix/trainers/_maskix_trainer.py +116 -0
- autoencodix/trainers/_ontix_trainer.py +77 -0
- autoencodix/trainers/_stackix_orchestrator.py +522 -0
- autoencodix/trainers/_stackix_trainer.py +209 -0
- autoencodix/trainers/_xmodal_trainer.py +1006 -0
- autoencodix/utils/.ipynb_checkpoints/_explainer-checkpoint.py +118 -0
- autoencodix/utils/.ipynb_checkpoints/_model_output-checkpoint.py +30 -0
- autoencodix/utils/.ipynb_checkpoints/_utils-checkpoint.py +626 -0
- autoencodix/utils/.ipynb_checkpoints/adata_converter-checkpoint.py +99 -0
- autoencodix/utils/.ipynb_checkpoints/feature_importance-checkpoint.py +823 -0
- autoencodix/utils/__init__.py +22 -0
- autoencodix/utils/_annealer.py +193 -0
- autoencodix/utils/_bulkreader.py +232 -0
- autoencodix/utils/_explainer.py +246 -0
- autoencodix/utils/_imgreader.py +404 -0
- autoencodix/utils/_internals.py +23 -0
- autoencodix/utils/_llm_explainer.py +318 -0
- autoencodix/utils/_losses.py +20 -0
- autoencodix/utils/_model_output.py +30 -0
- autoencodix/utils/_result.py +393 -0
- autoencodix/utils/_screader.py +76 -0
- autoencodix/utils/_traindynamics.py +187 -0
- autoencodix/utils/_utils.py +690 -0
- autoencodix/utils/adata_converter.py +111 -0
- autoencodix/utils/example_data.py +398 -0
- autoencodix/utils/feature_importance.py +829 -0
- autoencodix/utils/prompts.py +21 -0
- autoencodix/vanillix.py +141 -0
- autoencodix/varix.py +80 -0
- autoencodix/visualize/__init__.py +5 -0
- autoencodix/visualize/_general_visualizer.py +913 -0
- autoencodix/visualize/_imagix_visualizer.py +111 -0
- autoencodix/visualize/_xmodal_visualizer.py +841 -0
- autoencodix/visualize/visualize.py +1214 -0
- autoencodix/xmodalix.py +318 -0
- autoencodix-0.2.0.dist-info/METADATA +143 -0
- autoencodix-0.2.0.dist-info/RECORD +91 -0
- autoencodix-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1274 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Dict, Optional, Tuple, Type, Union, Any, Literal, List
|
|
3
|
+
import scipy
|
|
4
|
+
|
|
5
|
+
from unittest import result
|
|
6
|
+
import warnings
|
|
7
|
+
import anndata as ad # type: ignore
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import torch
|
|
11
|
+
from mudata import MuData # type: ignore
|
|
12
|
+
from torch.utils.data import Dataset
|
|
13
|
+
|
|
14
|
+
# ML evaluation
|
|
15
|
+
from sklearn import linear_model # type: ignore
|
|
16
|
+
from sklearn.base import ClassifierMixin, RegressorMixin, is_classifier, is_regressor # type: ignore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from autoencodix.data._datasetcontainer import DatasetContainer
|
|
20
|
+
from autoencodix.data._datasplitter import DataSplitter
|
|
21
|
+
from autoencodix.data.datapackage import DataPackage
|
|
22
|
+
|
|
23
|
+
# from autoencodix.evaluate.evaluate import Evaluator
|
|
24
|
+
from autoencodix.base._base_evaluator import BaseEvaluator
|
|
25
|
+
from autoencodix.utils._result import Result
|
|
26
|
+
from autoencodix.utils._utils import Loader, Saver, get_dataset
|
|
27
|
+
from autoencodix.utils.adata_converter import AnnDataConverter
|
|
28
|
+
from autoencodix.utils._explainer import FeatureImportanceExplainer
|
|
29
|
+
from autoencodix.utils._llm_explainer import LLMExplainer
|
|
30
|
+
from autoencodix.configs.default_config import DataCase, DataInfo, DefaultConfig
|
|
31
|
+
|
|
32
|
+
from ._base_autoencoder import BaseAutoencoder
|
|
33
|
+
from ._base_dataset import BaseDataset
|
|
34
|
+
from ._base_loss import BaseLoss
|
|
35
|
+
from ._base_preprocessor import BasePreprocessor
|
|
36
|
+
from ._base_trainer import BaseTrainer
|
|
37
|
+
from ._base_visualizer import BaseVisualizer
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BasePipeline(abc.ABC):
|
|
41
|
+
"""Provides a standardized interface for building model pipelines.
|
|
42
|
+
|
|
43
|
+
Implements methods for preprocessing data, training models, making predictions,
|
|
44
|
+
evaluating performance, and visualizing results. Subclasses customize behavior
|
|
45
|
+
by providing specific implementations for processing, training, evaluation,
|
|
46
|
+
and visualization. For example when using the Stackix Model, we would use
|
|
47
|
+
the StackixPreprocessor Type for preprocessing.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
config: Configuration for the pipeline's components and behavior.
|
|
51
|
+
preprocessed_data: Pre-split and processed data that can be provided by user.
|
|
52
|
+
raw_user_data: Raw input data for processing (DataFrames, MuData, etc.).
|
|
53
|
+
result: Storage container for all pipeline outputs.
|
|
54
|
+
_preprocessor: Component that filters, scales, and cleans data.
|
|
55
|
+
_visualizer: Component that generates visual representations of results.
|
|
56
|
+
_dataset_type: Base class for dataset implementations.
|
|
57
|
+
_trainer_type: Base class for trainer implementations.
|
|
58
|
+
_model_type: Base class for model architecture implementations.
|
|
59
|
+
_loss_type: Base class for loss function implementations.
|
|
60
|
+
_datasets: Split datasets after preprocessing.
|
|
61
|
+
_evaluator: Component that assesses model performance. Not implemented yet
|
|
62
|
+
_data_splitter: Component that divides data into train/validation/test sets.
|
|
63
|
+
_ontologies: Tuple of dictionaries containing the ontologies to be used to construct sparse decoder layers.
|
|
64
|
+
If a list is provided, it is assumed to be a list of file paths to ontology files.
|
|
65
|
+
First item in list or tuple will be treated as first layer (after latent space) and so on.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
dataset_type: Type[BaseDataset],
|
|
72
|
+
trainer_type: Type[BaseTrainer],
|
|
73
|
+
model_type: Type[BaseAutoencoder],
|
|
74
|
+
loss_type: Type[BaseLoss],
|
|
75
|
+
datasplitter_type: Type[DataSplitter],
|
|
76
|
+
preprocessor_type: Type[BasePreprocessor],
|
|
77
|
+
data: Optional[
|
|
78
|
+
Union[DataPackage, DatasetContainer, ad.AnnData, MuData, pd.DataFrame, dict] # type: ignore[invalid-type-form]
|
|
79
|
+
],
|
|
80
|
+
visualizer: Optional[BaseVisualizer] = None,
|
|
81
|
+
evaluator: Optional[BaseEvaluator] = None,
|
|
82
|
+
result: Optional[Result] = None,
|
|
83
|
+
config: Optional[DefaultConfig] = None,
|
|
84
|
+
custom_split: Optional[Dict[str, np.ndarray]] = None,
|
|
85
|
+
ontologies: Optional[Union[Tuple, Dict[Any, Any]]] = None,
|
|
86
|
+
**kwargs: dict,
|
|
87
|
+
) -> None: # ty: ignore[call-non-callable]
|
|
88
|
+
"""Initializes the pipeline with components and configuration.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
dataset_type: Class for dataset implementations.
|
|
92
|
+
trainer_type: Class for model training implementations.
|
|
93
|
+
model_type: Class for model architecture implementations.
|
|
94
|
+
loss_type: Class for loss function implementations.
|
|
95
|
+
datasplitter_type: Class for data splitting implementation.
|
|
96
|
+
preprocessor_type: Class for data preprocessing implementation.
|
|
97
|
+
visualizer: Component for generating visualizations.
|
|
98
|
+
data: Input data to be processed or already processed data.
|
|
99
|
+
evaluator: Component for assessing model performance.
|
|
100
|
+
result: Storage container for pipeline outputs.
|
|
101
|
+
config: Configuration parameters for all pipeline components.
|
|
102
|
+
custom_split: User-provided data splits (train/validation/test).
|
|
103
|
+
**kwargs: Additional keyword arguments.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
TypeError: If inputs have incorrect types.
|
|
107
|
+
"""
|
|
108
|
+
if not hasattr(self, "_default_config"):
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"""
|
|
111
|
+
The _default_config attribute has not been specified in your pipeline class.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
self._default_config = XModalixConfig()
|
|
115
|
+
|
|
116
|
+
This error typically occurs when a new architecture is added without setting the
|
|
117
|
+
_default_config in its corresponding pipeline class.
|
|
118
|
+
|
|
119
|
+
For more details, please refer to the 'how to add a new architecture' section in our documentation.
|
|
120
|
+
"""
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self._validate_config(config=config)
|
|
124
|
+
self._validate_user_input(data=data)
|
|
125
|
+
processed_data = data if isinstance(data, DatasetContainer) else None
|
|
126
|
+
raw_user_data = (
|
|
127
|
+
data
|
|
128
|
+
if isinstance(data, (DataPackage, ad.AnnData, MuData, pd.DataFrame, dict))
|
|
129
|
+
else None
|
|
130
|
+
)
|
|
131
|
+
if processed_data is not None and not isinstance(
|
|
132
|
+
processed_data, DatasetContainer
|
|
133
|
+
):
|
|
134
|
+
raise TypeError(
|
|
135
|
+
f"Expected data type to be DatasetContainer, got {type(processed_data)}."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self.preprocessed_data: Optional[DatasetContainer] = processed_data
|
|
139
|
+
self.raw_user_data: Union[
|
|
140
|
+
DataPackage, ad.AnnData, MuData, pd.DataFrame, dict # type: ignore[invalid-type-form]
|
|
141
|
+
] = raw_user_data
|
|
142
|
+
self._trainer_type = trainer_type
|
|
143
|
+
self._trainer: Optional[BaseTrainer] = None
|
|
144
|
+
self._model_type = model_type
|
|
145
|
+
self._loss_type = loss_type
|
|
146
|
+
self._preprocessor_type = preprocessor_type
|
|
147
|
+
if self.raw_user_data is not None:
|
|
148
|
+
self.raw_user_data, datacase = self._handle_direct_user_data(
|
|
149
|
+
data=self.raw_user_data,
|
|
150
|
+
)
|
|
151
|
+
self.config.data_case = datacase
|
|
152
|
+
self._fill_data_info()
|
|
153
|
+
|
|
154
|
+
self.ontologies = ontologies
|
|
155
|
+
self._preprocessor = self._preprocessor_type(
|
|
156
|
+
config=self.config, ontologies=self.ontologies
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
self.visualizer = (
|
|
160
|
+
visualizer() # ty: ignore[call-non-callable]
|
|
161
|
+
if visualizer is not None
|
|
162
|
+
else BaseVisualizer() # ty: ignore[call-non-callable]
|
|
163
|
+
) # ty: ignore[call-non-callable]
|
|
164
|
+
self.evaluator = (
|
|
165
|
+
evaluator() # ty: ignore[call-non-callable]
|
|
166
|
+
if evaluator is not None
|
|
167
|
+
else BaseEvaluator() # ty: ignore[call-non-callable]
|
|
168
|
+
) # ty: ignore[call-non-callable]
|
|
169
|
+
self.result = result if result is not None else Result()
|
|
170
|
+
self._dataset_type = dataset_type
|
|
171
|
+
self._data_splitter = datasplitter_type(
|
|
172
|
+
config=self.config, custom_splits=custom_split
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self._datasets: Optional[DatasetContainer] = (
|
|
176
|
+
processed_data # None, or user input
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _validate_config(self, config: Any) -> None:
|
|
180
|
+
"""Sets config to default if None, or validates its type.
|
|
181
|
+
Args:
|
|
182
|
+
config: Configuration object to validate or set to default.
|
|
183
|
+
Raises:
|
|
184
|
+
TypeError: If config is not of type DefaultConfig
|
|
185
|
+
"""
|
|
186
|
+
if config is None:
|
|
187
|
+
self.config = self._default_config # type: ignore
|
|
188
|
+
else:
|
|
189
|
+
if not isinstance(config, DefaultConfig):
|
|
190
|
+
raise TypeError(
|
|
191
|
+
f"Expected config type to be DefaultConfig, got {type(config)}."
|
|
192
|
+
)
|
|
193
|
+
if not isinstance(config, type(self._default_config)): # type: ignore
|
|
194
|
+
warnings.warn(
|
|
195
|
+
f"Your config is of type: {type(config)}, for this pipeline the default params of: {type(self._default_config)} work best"
|
|
196
|
+
)
|
|
197
|
+
self.config = config
|
|
198
|
+
|
|
199
|
+
def _validate_user_input(self, data: Any) -> None:
|
|
200
|
+
"""Ensures that user-provided data is of a valid type.
|
|
201
|
+
Args:
|
|
202
|
+
data: User-provided data to validate.
|
|
203
|
+
Raises:
|
|
204
|
+
TypeError: If data is not of a supported type.
|
|
205
|
+
"""
|
|
206
|
+
if not isinstance(
|
|
207
|
+
data,
|
|
208
|
+
(
|
|
209
|
+
DataPackage,
|
|
210
|
+
ad.AnnData,
|
|
211
|
+
MuData,
|
|
212
|
+
pd.DataFrame,
|
|
213
|
+
dict,
|
|
214
|
+
type(None),
|
|
215
|
+
DatasetContainer,
|
|
216
|
+
),
|
|
217
|
+
):
|
|
218
|
+
raise TypeError(
|
|
219
|
+
f"Expected data type to be one of [DataPackage, AnnData, MuData, "
|
|
220
|
+
f"pd.DataFrame, dict, DatasetContainer], got {type(data)}."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _handle_direct_user_data(
|
|
224
|
+
self,
|
|
225
|
+
data,
|
|
226
|
+
) -> Tuple[DataPackage, DataCase]:
|
|
227
|
+
"""Converts raw user data into a standardized DataPackage format.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
data: Raw input data in various formats.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
DataPackage containing the standardized data
|
|
234
|
+
DataCase, muliti_single_cell or multi_bulk, etc.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
TypeError: If data format is not supported.
|
|
238
|
+
ValueError: If data doesn't meet format requirements or data_case
|
|
239
|
+
cannot be inferred.
|
|
240
|
+
"""
|
|
241
|
+
print(f"in handle_direct_user_data with data: {type(data)}")
|
|
242
|
+
data_case = self.config.data_case
|
|
243
|
+
if isinstance(data, DataPackage):
|
|
244
|
+
data_package = data
|
|
245
|
+
data_case = self.config.data_case
|
|
246
|
+
elif isinstance(data, ad.AnnData):
|
|
247
|
+
mudata = MuData({"user-data": data})
|
|
248
|
+
data_package = DataPackage(multi_sc={"multi_sc": mudata})
|
|
249
|
+
if self.config.data_case is None:
|
|
250
|
+
data_case = DataCase.MULTI_SINGLE_CELL
|
|
251
|
+
elif isinstance(data, MuData):
|
|
252
|
+
data_package = DataPackage(multi_sc={"multi_sc": data})
|
|
253
|
+
if self.config.data_case is None:
|
|
254
|
+
data_case = DataCase.MULTI_SINGLE_CELL
|
|
255
|
+
elif isinstance(data, pd.DataFrame):
|
|
256
|
+
data_package = DataPackage(multi_bulk={"user-data": data})
|
|
257
|
+
if self.config.data_case is None:
|
|
258
|
+
data_case = DataCase.MULTI_BULK
|
|
259
|
+
elif isinstance(data, dict):
|
|
260
|
+
# Check if all values in the dictionary are pandas DataFrames
|
|
261
|
+
if all(isinstance(value, pd.DataFrame) for value in data.values()):
|
|
262
|
+
data_package = DataPackage(multi_bulk=data)
|
|
263
|
+
if self.config.data_case is None:
|
|
264
|
+
data_case = DataCase.MULTI_BULK
|
|
265
|
+
else:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
"All values in the dictionary must be pandas DataFrames."
|
|
268
|
+
)
|
|
269
|
+
if data_case is None:
|
|
270
|
+
raise ValueError("data_case must be provided if it cannot be inferred.")
|
|
271
|
+
|
|
272
|
+
return data_package, data_case
|
|
273
|
+
|
|
274
|
+
def _validate_raw_user_data(self) -> None:
|
|
275
|
+
"""Validates the format and content of user-provided raw data.
|
|
276
|
+
|
|
277
|
+
Ensures that raw_user_data is a valid DataPackage with properly formatted
|
|
278
|
+
attributes.
|
|
279
|
+
|
|
280
|
+
Raises:
|
|
281
|
+
TypeError: If raw_user_data is not a DataPackage.
|
|
282
|
+
ValueError: If DataPackage attributes aren't dictionaries or all are None.
|
|
283
|
+
"""
|
|
284
|
+
if not isinstance(self.raw_user_data, DataPackage):
|
|
285
|
+
raise TypeError(
|
|
286
|
+
f"Expected raw_user_data to be of type DataPackage, got "
|
|
287
|
+
f"{type(self.raw_user_data)}."
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
all_none = True
|
|
291
|
+
for attr_name in self.raw_user_data.__annotations__:
|
|
292
|
+
attr_value = getattr(self.raw_user_data, attr_name)
|
|
293
|
+
if attr_value is not None:
|
|
294
|
+
all_none = False
|
|
295
|
+
if not isinstance(attr_value, dict):
|
|
296
|
+
raise ValueError(
|
|
297
|
+
f"Attribute '{attr_name}' of raw_user_data must be a dictionary, "
|
|
298
|
+
f"got {type(attr_value)}."
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if all_none:
|
|
302
|
+
raise ValueError(
|
|
303
|
+
"All attributes of raw_user_data are None. At least one must be non-None."
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _fill_data_info(self) -> None:
|
|
307
|
+
"""Populates the config's data_info with entries for all data keys.
|
|
308
|
+
|
|
309
|
+
Creates DataInfo objects for each data key found in raw_user_data
|
|
310
|
+
if they don't already exist in the configuration.
|
|
311
|
+
This method is needed, when the user provides data via the Pipeline and
|
|
312
|
+
not via the config.
|
|
313
|
+
"""
|
|
314
|
+
all_keys = []
|
|
315
|
+
for k in self.raw_user_data.__annotations__:
|
|
316
|
+
attr_value = getattr(self.raw_user_data, k)
|
|
317
|
+
all_keys.append(k)
|
|
318
|
+
if isinstance(attr_value, dict):
|
|
319
|
+
all_keys.extend(attr_value.keys())
|
|
320
|
+
for k, v in attr_value.items():
|
|
321
|
+
if isinstance(v, MuData):
|
|
322
|
+
all_keys.extend(v.mod.keys())
|
|
323
|
+
for k in all_keys:
|
|
324
|
+
if self.config.data_config.data_info.get(k) is None:
|
|
325
|
+
self.config.data_config.data_info[k] = DataInfo()
|
|
326
|
+
|
|
327
|
+
def _validate_user_data(self):
|
|
328
|
+
"""Validates user-provided data based on its source and format.
|
|
329
|
+
|
|
330
|
+
Performs different validation based on whether the user provided
|
|
331
|
+
preprocessed data, raw data, or a data configuration.
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
Various exceptions depending on validation results.
|
|
335
|
+
"""
|
|
336
|
+
if self.raw_user_data is None:
|
|
337
|
+
if self._datasets is not None: # case when user passes preprocessed data
|
|
338
|
+
self._validate_container()
|
|
339
|
+
else: # user passes data via config
|
|
340
|
+
self._validate_config_data()
|
|
341
|
+
else:
|
|
342
|
+
self._validate_raw_user_data()
|
|
343
|
+
|
|
344
|
+
def _validate_container(self):
|
|
345
|
+
"""Validates that a DatasetContainer has at least one valid dataset.
|
|
346
|
+
|
|
347
|
+
Ensures the container has properly formatted datasets and at least
|
|
348
|
+
one split is present.
|
|
349
|
+
|
|
350
|
+
Raises:
|
|
351
|
+
ValueError: If container validation fails.
|
|
352
|
+
"""
|
|
353
|
+
if self.preprocessed_data is None:
|
|
354
|
+
raise ValueError("DatasetContainer is None. Please provide valid datasets.")
|
|
355
|
+
none_count = 0
|
|
356
|
+
if not isinstance(self.preprocessed_data.train, Dataset):
|
|
357
|
+
if self.preprocessed_data.train is not None:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
f"Train dataset has to be either None or Dataset, got "
|
|
360
|
+
f"{type(self.preprocessed_data.train)}"
|
|
361
|
+
)
|
|
362
|
+
none_count += 1
|
|
363
|
+
if not isinstance(self.preprocessed_data.test, Dataset):
|
|
364
|
+
if self.preprocessed_data.test is not None:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"Test dataset has to be either None or Dataset, got "
|
|
367
|
+
f"{type(self.preprocessed_data.test)}"
|
|
368
|
+
)
|
|
369
|
+
none_count += 1
|
|
370
|
+
|
|
371
|
+
if not isinstance(self.preprocessed_data.valid, Dataset):
|
|
372
|
+
if self.preprocessed_data.valid is not None:
|
|
373
|
+
raise ValueError(
|
|
374
|
+
f"Valid dataset has to be either None or Dataset, got "
|
|
375
|
+
f"{type(self.preprocessed_data.valid)}"
|
|
376
|
+
)
|
|
377
|
+
none_count += 1
|
|
378
|
+
if none_count == 3:
|
|
379
|
+
raise ValueError("At least one split needs to be provided")
|
|
380
|
+
|
|
381
|
+
def _validate_config_data(self):
|
|
382
|
+
"""Validates the data configuration provided via config.
|
|
383
|
+
|
|
384
|
+
Ensures the data configuration has the necessary components based
|
|
385
|
+
on the data types being processed.
|
|
386
|
+
|
|
387
|
+
Raises:
|
|
388
|
+
ValueError: If data configuration validation fails.
|
|
389
|
+
"""
|
|
390
|
+
data_info_dict = self.config.data_config.data_info
|
|
391
|
+
if not data_info_dict:
|
|
392
|
+
raise ValueError("data_info dictionary is empty.")
|
|
393
|
+
|
|
394
|
+
# Check if there's at least one non-annotation file
|
|
395
|
+
non_annotation_files = {
|
|
396
|
+
key: info
|
|
397
|
+
for key, info in data_info_dict.items()
|
|
398
|
+
if info.data_type != "ANNOTATION"
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if not non_annotation_files:
|
|
402
|
+
raise ValueError("At least one non-annotation file must be provided.")
|
|
403
|
+
|
|
404
|
+
# Check if there's any non-single-cell data
|
|
405
|
+
non_single_cell_data = {
|
|
406
|
+
key: info
|
|
407
|
+
for key, info in data_info_dict.items()
|
|
408
|
+
if not info.is_single_cell and info.data_type != "ANNOTATION"
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# If there's non-single-cell data, check for annotation file
|
|
412
|
+
if non_single_cell_data:
|
|
413
|
+
annotation_files = {
|
|
414
|
+
key: info
|
|
415
|
+
for key, info in data_info_dict.items()
|
|
416
|
+
if info.data_type == "ANNOTATION"
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if not annotation_files:
|
|
420
|
+
raise ValueError(
|
|
421
|
+
"When working with non-single-cell data, an annotation file must be "
|
|
422
|
+
"provided."
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def preprocess(self, config: Optional[Union[None, DefaultConfig]] = None, **kwargs):
|
|
426
|
+
"""Filters, normalizes and prepares data for model training.
|
|
427
|
+
|
|
428
|
+
Processes raw input data into the format required by the model and creates
|
|
429
|
+
train/validation/test splits as needed.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
config: Optional custom configuration for preprocessing.
|
|
433
|
+
**kwargs: Additional configuration parameters as keyword arguments.
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
NotImplementedError: If preprocessor is not initialized.
|
|
437
|
+
"""
|
|
438
|
+
if self._preprocessor_type is None:
|
|
439
|
+
raise NotImplementedError("Preprocessor not initialized")
|
|
440
|
+
self._validate_user_data()
|
|
441
|
+
if self.preprocessed_data is None:
|
|
442
|
+
self.preprocessed_data = self._preprocessor.preprocess(
|
|
443
|
+
raw_user_data=self.raw_user_data, # type: ignore
|
|
444
|
+
)
|
|
445
|
+
self.result.datasets = self.preprocessed_data
|
|
446
|
+
self._datasets = self.preprocessed_data
|
|
447
|
+
else:
|
|
448
|
+
self._datasets = self.preprocessed_data
|
|
449
|
+
self.result.datasets = self.preprocessed_data
|
|
450
|
+
|
|
451
|
+
def fit(self, config: Optional[Union[None, DefaultConfig]] = None, **kwargs):
|
|
452
|
+
"""Trains the model on preprocessed data.
|
|
453
|
+
|
|
454
|
+
Creates and configures a trainer instance, then executes the training
|
|
455
|
+
process using the preprocessed datasets.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
config: Optional custom configuration for training.
|
|
459
|
+
**kwargs: Additional configuration parameters as keyword arguments.
|
|
460
|
+
|
|
461
|
+
Raises:
|
|
462
|
+
ValueError: If datasets aren't available for training.
|
|
463
|
+
"""
|
|
464
|
+
if self._datasets is None:
|
|
465
|
+
raise ValueError(
|
|
466
|
+
"Datasets not built. Please run the preprocess method first."
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
self._trainer = self._trainer_type(
|
|
470
|
+
trainset=self._datasets.train,
|
|
471
|
+
validset=self._datasets.valid,
|
|
472
|
+
result=self.result,
|
|
473
|
+
config=self.config,
|
|
474
|
+
model_type=self._model_type,
|
|
475
|
+
loss_type=self._loss_type,
|
|
476
|
+
ontologies=self.ontologies, # Ontix
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
trainer_result: Result = self._trainer.train()
|
|
480
|
+
self.result.update(other=trainer_result)
|
|
481
|
+
|
|
482
|
+
def predict(
|
|
483
|
+
self,
|
|
484
|
+
data: Optional[
|
|
485
|
+
Union[
|
|
486
|
+
DataPackage,
|
|
487
|
+
DatasetContainer,
|
|
488
|
+
ad.AnnData,
|
|
489
|
+
MuData, # ty: ignore[invalid-type-form]
|
|
490
|
+
] # ty: ignore[invalid-type-form]
|
|
491
|
+
] = None, # ty: ignore[invalid-type-form]
|
|
492
|
+
config: Optional[Union[None, DefaultConfig]] = None,
|
|
493
|
+
from_key: Optional[str] = None,
|
|
494
|
+
to_key: Optional[str] = None,
|
|
495
|
+
**kwargs,
|
|
496
|
+
):
|
|
497
|
+
"""Generates predictions using the trained model.
|
|
498
|
+
|
|
499
|
+
Uses the trained model to make predictions on test data or new data
|
|
500
|
+
provided by the user. Processes the results and stores them in the
|
|
501
|
+
result container.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
data: Optional new data for predictions.
|
|
505
|
+
config: Optional custom configuration for prediction.
|
|
506
|
+
**kwargs: Additional configuration parameters as keyword arguments.
|
|
507
|
+
|
|
508
|
+
Raises:
|
|
509
|
+
NotImplementedError: If required components aren't initialized.
|
|
510
|
+
ValueError: If no test data is available or data format is invalid.
|
|
511
|
+
"""
|
|
512
|
+
self._validate_prediction_requirements()
|
|
513
|
+
if self._trainer is None:
|
|
514
|
+
raise ValueError(
|
|
515
|
+
"Trainer not initialized, call fit first. If you used .save and .load, then you shoul not call .fit, then this is a bug."
|
|
516
|
+
"In this case please submit an issue."
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
self._trainer.setup_trainer(old_model=self.result.model)
|
|
520
|
+
original_input = data
|
|
521
|
+
predict_data = self._prepare_prediction_data(data=data)
|
|
522
|
+
|
|
523
|
+
predictor_results = self._generate_predictions(
|
|
524
|
+
predict_data=predict_data,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
self._process_latent_results(
|
|
528
|
+
predictor_results=predictor_results, predict_data=predict_data
|
|
529
|
+
)
|
|
530
|
+
self._postprocess_reconstruction(
|
|
531
|
+
predictor_results=predictor_results,
|
|
532
|
+
original_input=original_input,
|
|
533
|
+
predict_data=predict_data,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
return self.result
|
|
537
|
+
|
|
538
|
+
def _validate_prediction_requirements(self):
|
|
539
|
+
"""Validate that required components are initialized."""
|
|
540
|
+
if self._preprocessor is None:
|
|
541
|
+
raise NotImplementedError("Preprocessor not initialized")
|
|
542
|
+
if self.result.model is None:
|
|
543
|
+
raise NotImplementedError(
|
|
544
|
+
"Model not trained. Please run the fit method first"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
def _prepare_prediction_data(
|
|
548
|
+
self,
|
|
549
|
+
data: Optional[
|
|
550
|
+
Union[
|
|
551
|
+
DataPackage,
|
|
552
|
+
DatasetContainer,
|
|
553
|
+
ad.AnnData,
|
|
554
|
+
MuData, # ty: ignore[invalid-type-form]
|
|
555
|
+
] # ty: ignore[invalid-type-form]
|
|
556
|
+
] = None, # ty: ignore[invalid-type-form]
|
|
557
|
+
) -> DatasetContainer:
|
|
558
|
+
"""Prepare and validate input data for prediction.
|
|
559
|
+
Args:
|
|
560
|
+
data: Optional new data for predictions. If None, uses existing datasets.
|
|
561
|
+
Returns:
|
|
562
|
+
DatasetContainer: The prepared dataset container for predictions.
|
|
563
|
+
Raises:
|
|
564
|
+
ValueError: If data type is unsupported or no test data is available.
|
|
565
|
+
"""
|
|
566
|
+
if data is None:
|
|
567
|
+
return self._get_existing_datasets()
|
|
568
|
+
elif isinstance(data, DatasetContainer):
|
|
569
|
+
return self._handle_dataset_container(data=data)
|
|
570
|
+
elif isinstance(data, (DataPackage, ad.AnnData, MuData, dict, pd.DataFrame)):
|
|
571
|
+
return self._handle_user_data(data=data)
|
|
572
|
+
else:
|
|
573
|
+
raise ValueError(f"Unsupported data type: {type(data)}")
|
|
574
|
+
|
|
575
|
+
def _get_existing_datasets(self) -> DatasetContainer:
|
|
576
|
+
"""Get existing preprocessed datasets and validate them for prediction.
|
|
577
|
+
Returns:
|
|
578
|
+
DatasetContainer: The preprocessed datasets available for prediction.
|
|
579
|
+
Raises:
|
|
580
|
+
ValueError: If no datasets are available or no test data is present.
|
|
581
|
+
"""
|
|
582
|
+
if self._datasets is None:
|
|
583
|
+
raise ValueError(
|
|
584
|
+
"No data provided for prediction and no preprocessed datasets "
|
|
585
|
+
"available. Please run the preprocess method first or provide "
|
|
586
|
+
"data for prediction."
|
|
587
|
+
)
|
|
588
|
+
if self._datasets.test is None:
|
|
589
|
+
raise ValueError("No test data available for prediction")
|
|
590
|
+
return self._datasets
|
|
591
|
+
|
|
592
|
+
def _handle_dataset_container(self, data: DatasetContainer) -> DatasetContainer:
|
|
593
|
+
"""Handle DatasetContainer input for prediction.
|
|
594
|
+
Args:
|
|
595
|
+
data: DatasetContainer containing preprocessed datasets.
|
|
596
|
+
Returns:
|
|
597
|
+
DatasetContainer: The processed dataset container for predictions.
|
|
598
|
+
"""
|
|
599
|
+
self.result.new_datasets = data
|
|
600
|
+
|
|
601
|
+
if hasattr(self._preprocessor, "_dataset_container"):
|
|
602
|
+
self._preprocessor._dataset_container = data
|
|
603
|
+
|
|
604
|
+
return data
|
|
605
|
+
|
|
606
|
+
def _handle_user_data(self, data: Any) -> DatasetContainer:
|
|
607
|
+
"""Handle user-provided data (DataPackage, AnnData, etc.).
|
|
608
|
+
Args:
|
|
609
|
+
data: Raw user data in various formats (DataPackage, AnnData, etc.).
|
|
610
|
+
Returns:
|
|
611
|
+
DatasetContainer: The processed dataset container for predictions.
|
|
612
|
+
Raises:
|
|
613
|
+
ValueError: If data type is unsupported or no test data is available.
|
|
614
|
+
"""
|
|
615
|
+
processed_data, _ = self._handle_direct_user_data(data=data)
|
|
616
|
+
predict_data = self._preprocessor.preprocess(
|
|
617
|
+
raw_user_data=processed_data, predict_new_data=True
|
|
618
|
+
)
|
|
619
|
+
self.result.new_datasets = predict_data
|
|
620
|
+
return predict_data
|
|
621
|
+
|
|
622
|
+
def _validate_prediction_data(self, predict_data: DatasetContainer):
|
|
623
|
+
"""Validate that prediction data has required test split."""
|
|
624
|
+
if predict_data.test is None:
|
|
625
|
+
raise ValueError(
|
|
626
|
+
f"The data for prediction need to be a DatasetContainer with a test "
|
|
627
|
+
f"attribute, got: {predict_data}"
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
def _generate_predictions(
|
|
631
|
+
self,
|
|
632
|
+
predict_data: DatasetContainer,
|
|
633
|
+
):
|
|
634
|
+
"""Generate predictions using the trained model.
|
|
635
|
+
Args:
|
|
636
|
+
predict_data: DatasetContainer with preprocessed datasets for prediction.
|
|
637
|
+
Returns:
|
|
638
|
+
Predictor results containing latent spaces and reconstructions.
|
|
639
|
+
|
|
640
|
+
"""
|
|
641
|
+
self._validate_prediction_data(predict_data=predict_data)
|
|
642
|
+
return self._trainer.predict(
|
|
643
|
+
data=predict_data.test,
|
|
644
|
+
model=self.result.model,
|
|
645
|
+
) # type: ignore
|
|
646
|
+
|
|
647
|
+
def _process_latent_results(
|
|
648
|
+
self, predictor_results, predict_data: DatasetContainer
|
|
649
|
+
):
|
|
650
|
+
"""Process and store latent space results.
|
|
651
|
+
Args:
|
|
652
|
+
predictor_results: Results from the prediction step containing latents.
|
|
653
|
+
predict_data: DatasetContainer with preprocessed datasets for prediction.
|
|
654
|
+
"""
|
|
655
|
+
latent = predictor_results.latentspaces.get(epoch=-1, split="test")
|
|
656
|
+
if isinstance(latent, dict):
|
|
657
|
+
print("Detected dictionary in latent results, extracting array...")
|
|
658
|
+
latent = next(iter(latent.values())) # TODO better adjust for xmodal
|
|
659
|
+
self.result.adata_latent = ad.AnnData(latent)
|
|
660
|
+
self.result.adata_latent.obs_names = predict_data.test.sample_ids # type: ignore
|
|
661
|
+
self.result.adata_latent.uns["var_names"] = predict_data.test.feature_ids # type: ignore
|
|
662
|
+
self.result.update(predictor_results)
|
|
663
|
+
|
|
664
|
+
def _postprocess_reconstruction(
|
|
665
|
+
self, predictor_results, original_input, predict_data: DatasetContainer
|
|
666
|
+
):
|
|
667
|
+
"""Postprocess reconstruction results based on input type.
|
|
668
|
+
|
|
669
|
+
This outpus the reconstruction in the same format as the original input data,
|
|
670
|
+
whether it is a DatasetContainer, DataPackage, AnnData, MuData, or other formats.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
predictor_results: Results from the prediction step containing reconstructions.
|
|
674
|
+
original_input: Original input data format (if provided).
|
|
675
|
+
predict_data: DatasetContainer with preprocessed datasets for prediction.
|
|
676
|
+
Raises:
|
|
677
|
+
ValueError: If reconstruction fails or data types are incompatible.
|
|
678
|
+
"""
|
|
679
|
+
raw_recon: Union[Dict, np.ndarray, torch.Tensor] = (
|
|
680
|
+
self.result.reconstructions.get(epoch=-1, split="test")
|
|
681
|
+
)
|
|
682
|
+
if isinstance(raw_recon, np.ndarray):
|
|
683
|
+
raw_recon = torch.from_numpy(raw_recon) # type: ignore
|
|
684
|
+
elif isinstance(raw_recon, dict):
|
|
685
|
+
raw_recon = raw_recon.get("translation") # type: ignore
|
|
686
|
+
if raw_recon is None:
|
|
687
|
+
raise ValueError(
|
|
688
|
+
f"Raw recon is dict, but has no translation key, this should not happen: {raw_recon}"
|
|
689
|
+
)
|
|
690
|
+
raw_recon = torch.from_numpy(raw_recon) # type: ignore
|
|
691
|
+
else:
|
|
692
|
+
raise ValueError(
|
|
693
|
+
f"type of raw_recon has to be 'dict' or 'np.ndarray', got: {type(raw_recon)}"
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
if original_input is None:
|
|
697
|
+
# Using existing datasets
|
|
698
|
+
self._handle_dataset_container_reconstruction(
|
|
699
|
+
raw_recon=raw_recon, # type: ignore
|
|
700
|
+
dataset_container=predict_data,
|
|
701
|
+
context="existing datasets",
|
|
702
|
+
)
|
|
703
|
+
elif isinstance(original_input, DatasetContainer):
|
|
704
|
+
self._handle_dataset_container_reconstruction(
|
|
705
|
+
raw_recon=raw_recon, # type: ignore
|
|
706
|
+
dataset_container=original_input,
|
|
707
|
+
context="provided DatasetContainer",
|
|
708
|
+
)
|
|
709
|
+
elif self.config.data_case == DataCase.MULTI_SINGLE_CELL:
|
|
710
|
+
self._handle_multi_single_cell_reconstruction(
|
|
711
|
+
raw_recon=raw_recon,
|
|
712
|
+
predictor_results=predictor_results, # type: ignore
|
|
713
|
+
)
|
|
714
|
+
elif isinstance(
|
|
715
|
+
original_input, (DataPackage, ad.AnnData, MuData, dict, pd.DataFrame)
|
|
716
|
+
):
|
|
717
|
+
self._handle_user_data_reconstruction(
|
|
718
|
+
raw_recon=raw_recon, predictor_results=predictor_results
|
|
719
|
+
)
|
|
720
|
+
else:
|
|
721
|
+
self._handle_unsupported_reconstruction()
|
|
722
|
+
|
|
723
|
+
def _handle_dataset_container_reconstruction(
|
|
724
|
+
self,
|
|
725
|
+
raw_recon: torch.Tensor,
|
|
726
|
+
dataset_container: DatasetContainer,
|
|
727
|
+
context: str = "DatasetContainer",
|
|
728
|
+
):
|
|
729
|
+
"""Handle reconstruction for DatasetContainer input.
|
|
730
|
+
Args:
|
|
731
|
+
raw_recon: Raw reconstruction tensor from the model.
|
|
732
|
+
dataset_container: Original DatasetContainer provided by the user.
|
|
733
|
+
context: Description of the data context for error messages.
|
|
734
|
+
Raises:
|
|
735
|
+
ValueError: If no test data is available in the container.
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
# if dataset_container.test is None:
|
|
739
|
+
# raise ValueError(f"No test data available in {context} for reconstruction.")
|
|
740
|
+
# temp = copy.deepcopy(dataset_container.test)
|
|
741
|
+
# temp.data = raw_recon
|
|
742
|
+
# self.result.final_reconstruction = temp
|
|
743
|
+
pass
|
|
744
|
+
|
|
745
|
+
def _handle_multi_single_cell_reconstruction(
|
|
746
|
+
self, raw_recon: torch.Tensor, predictor_results: Result
|
|
747
|
+
):
|
|
748
|
+
"""Handle reconstruction for multi-single-cell data
|
|
749
|
+
Args:
|
|
750
|
+
raw_recon: Raw reconstruction tensor from the model.
|
|
751
|
+
predictor_results: Results from the prediction step containing reconstructions.
|
|
752
|
+
Raises:
|
|
753
|
+
ValueError: If reconstruction formatting fails or data types are incompatible.
|
|
754
|
+
"""
|
|
755
|
+
pkg = self._preprocessor.format_reconstruction(
|
|
756
|
+
reconstruction=raw_recon, result=predictor_results
|
|
757
|
+
)
|
|
758
|
+
if not isinstance(pkg.multi_sc, dict):
|
|
759
|
+
raise ValueError(
|
|
760
|
+
"Expected pkg.multi_sc to be a dictionary, got "
|
|
761
|
+
f"{type(pkg.multi_sc)} instead."
|
|
762
|
+
)
|
|
763
|
+
self.result.final_reconstruction = pkg.multi_sc["multi_sc"]
|
|
764
|
+
|
|
765
|
+
def _handle_user_data_reconstruction(
|
|
766
|
+
self, raw_recon: torch.Tensor, predictor_results
|
|
767
|
+
):
|
|
768
|
+
"""Handle reconstruction for user-provided data formats.
|
|
769
|
+
Args:
|
|
770
|
+
raw_recon: Raw reconstruction tensor from the model.
|
|
771
|
+
predictor_results: Results from the prediction step containing reconstructions.
|
|
772
|
+
"""
|
|
773
|
+
pkg = self._preprocessor.format_reconstruction(
|
|
774
|
+
reconstruction=raw_recon, result=predictor_results
|
|
775
|
+
)
|
|
776
|
+
self.result.final_reconstruction = pkg
|
|
777
|
+
|
|
778
|
+
def _handle_unsupported_reconstruction(self):
|
|
779
|
+
"""Handle cases where reconstruction formatting is not available."""
|
|
780
|
+
print(
|
|
781
|
+
"Reconstruction Formatting (the process of using the reconstruction "
|
|
782
|
+
"output of the autoencoder models and combine it with metadata to get "
|
|
783
|
+
"the exact same data structure as the raw input data i.e, a DataPackage, "
|
|
784
|
+
"DatasetContainer, or AnnData) not available for this data type or case."
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
def decode(
|
|
788
|
+
self, latent: Union[torch.Tensor, ad.AnnData, pd.DataFrame]
|
|
789
|
+
) -> Union[torch.Tensor, ad.AnnData, pd.DataFrame]:
|
|
790
|
+
"""Transforms latent space representations back to input space.
|
|
791
|
+
|
|
792
|
+
Handles various input formats for the latent representation and
|
|
793
|
+
returns the decoded data in a matching format.
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
latent: Latent space representation to decode.
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
Decoded data in a format matching the input.
|
|
800
|
+
|
|
801
|
+
Raises:
|
|
802
|
+
TypeError: If no model has been trained or input type is invalid.
|
|
803
|
+
ValueError: If latent dimensions are incompatible with the model.
|
|
804
|
+
"""
|
|
805
|
+
if self.result.model is None:
|
|
806
|
+
raise TypeError("No model trained yet, use fit() or run() method first")
|
|
807
|
+
recons: torch.Tensor
|
|
808
|
+
if isinstance(latent, ad.AnnData):
|
|
809
|
+
latent_data = torch.tensor(
|
|
810
|
+
latent.X, dtype=torch.float32
|
|
811
|
+
) # Ensure float for compatibility
|
|
812
|
+
|
|
813
|
+
expected_latent_dim = self.config.latent_dim
|
|
814
|
+
if not latent_data.shape[1] == expected_latent_dim:
|
|
815
|
+
raise ValueError(
|
|
816
|
+
f"Input AnnData's .X has shape {latent_data.shape}, but the model "
|
|
817
|
+
f"expects a latent vector of size {expected_latent_dim}. Consider "
|
|
818
|
+
f"projecting the AnnData to the correct latent space first."
|
|
819
|
+
)
|
|
820
|
+
latent_tensor = latent_data
|
|
821
|
+
|
|
822
|
+
recons = self._trainer.decode(x=latent_tensor)
|
|
823
|
+
if self._datasets is None:
|
|
824
|
+
raise ValueError(
|
|
825
|
+
"No datasets available in the DatasetContainer to reconstruct "
|
|
826
|
+
"AnnData objects. Please provide a valid DatasetContainer."
|
|
827
|
+
)
|
|
828
|
+
if self._datasets.train is None:
|
|
829
|
+
raise ValueError(
|
|
830
|
+
"The train dataset in the DatasetContainer is None. "
|
|
831
|
+
"Please provide a valid train dataset to reconstruct AnnData objects."
|
|
832
|
+
)
|
|
833
|
+
if not isinstance(self._datasets.train, BaseDataset):
|
|
834
|
+
raise TypeError(
|
|
835
|
+
"The train dataset in the DatasetContainer must be a BaseDataset "
|
|
836
|
+
"to reconstruct AnnData objects."
|
|
837
|
+
)
|
|
838
|
+
recons_adata = ad.AnnData(
|
|
839
|
+
X=recons.to("cpu").detach().numpy(),
|
|
840
|
+
obs=pd.DataFrame(index=latent.obs_names),
|
|
841
|
+
var=pd.DataFrame(index=self._datasets.train.feature_ids),
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
return recons_adata
|
|
845
|
+
elif isinstance(latent, pd.DataFrame):
|
|
846
|
+
latent_tensor = torch.tensor(latent.values, dtype=torch.float32)
|
|
847
|
+
recons = self._trainer.decode(x=latent_tensor)
|
|
848
|
+
return pd.DataFrame(
|
|
849
|
+
recons.to("cpu").detach().numpy(),
|
|
850
|
+
index=latent.index,
|
|
851
|
+
columns=latent.columns,
|
|
852
|
+
)
|
|
853
|
+
elif isinstance(latent, torch.Tensor):
|
|
854
|
+
# Check size compatibility
|
|
855
|
+
expected_latent_dim = self.config.latent_dim
|
|
856
|
+
if not latent.shape[1] == expected_latent_dim:
|
|
857
|
+
if self._trainer._model._mu.in_features == latent.shape[1]:
|
|
858
|
+
warnings.warn(
|
|
859
|
+
f"latent_prior has latent dimension {latent.shape[1]}, "
|
|
860
|
+
"which matches the input feature dimension of the model. Did you "
|
|
861
|
+
"mean to provide latent vectors of dimension "
|
|
862
|
+
"For Ontix this is the default behaviour and the warning can be ignored. "
|
|
863
|
+
f"{self.config.latent_dim}?"
|
|
864
|
+
)
|
|
865
|
+
else:
|
|
866
|
+
raise ValueError(
|
|
867
|
+
f"latent_prior has incompatible latent dimension {latent.shape[1]}, "
|
|
868
|
+
f"expected {self.config.latent_dim}."
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
latent_tensor = latent
|
|
872
|
+
else:
|
|
873
|
+
raise TypeError(
|
|
874
|
+
f"Input 'latent' must be either a torch.Tensor or an AnnData object, "
|
|
875
|
+
f"not {type(latent)}."
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
return self._trainer.decode(x=latent_tensor)
|
|
879
|
+
|
|
880
|
+
def evaluate(
|
|
881
|
+
self,
|
|
882
|
+
ml_model_class: ClassifierMixin = linear_model.LogisticRegression(),
|
|
883
|
+
ml_model_regression: RegressorMixin = linear_model.LinearRegression(),
|
|
884
|
+
params: Union[
|
|
885
|
+
list, str
|
|
886
|
+
] = [], # Default empty list, to use all parameters use string "all"
|
|
887
|
+
metric_class: str = "roc_auc_ovo", # Default is 'roc_auc_ovo' via https://scikit-learn.org/stable/modules/model_evaluation.html#scoring-string-names
|
|
888
|
+
metric_regression: str = "r2", # Default is 'r2'
|
|
889
|
+
reference_methods: list = [], # Default [], Options are "PCA", "UMAP", "TSNE", "RandomFeature"
|
|
890
|
+
split_type: Literal[
|
|
891
|
+
"use-split", "CV-5", "LOOC"
|
|
892
|
+
] = "use-split", # Default is "use-split", other options: "CV-5", ... "LOOCV"?
|
|
893
|
+
n_downsample: Optional[int] = 10000,
|
|
894
|
+
) -> Result:
|
|
895
|
+
"""TODO"""
|
|
896
|
+
if self.evaluator is None:
|
|
897
|
+
raise NotImplementedError("Evaluator not initialized")
|
|
898
|
+
if self.result.model is None:
|
|
899
|
+
raise NotImplementedError(
|
|
900
|
+
"Model not trained. Please run the fit method first"
|
|
901
|
+
)
|
|
902
|
+
if not is_classifier(ml_model_class):
|
|
903
|
+
warnings.warn(
|
|
904
|
+
"The provided model is not a sklearn-type classifier. "
|
|
905
|
+
"Evaluation continues but may produce incorrect results or errors."
|
|
906
|
+
)
|
|
907
|
+
if not is_regressor(ml_model_regression):
|
|
908
|
+
warnings.warn(
|
|
909
|
+
"The provided model is not a sklearn-type regressor. "
|
|
910
|
+
"Evaluation continues but may produce incorrect results or errors."
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
if len(params) == 0:
|
|
914
|
+
if self.config.data_config.annotation_columns is None:
|
|
915
|
+
params = [] # type: ignore
|
|
916
|
+
else:
|
|
917
|
+
params = self.config.data_config.annotation_columns # type: ignore
|
|
918
|
+
|
|
919
|
+
if len(params) == 0:
|
|
920
|
+
raise ValueError(
|
|
921
|
+
"No parameters specified for evaluation. Please provide a list of "
|
|
922
|
+
"parameters or ensure that annotation_columns are set in the config."
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
if "RandomFeature" in reference_methods:
|
|
926
|
+
if self._datasets is None:
|
|
927
|
+
raise ValueError(
|
|
928
|
+
"Datasets not available for adding RandomFeature. Please keep "
|
|
929
|
+
"preprocessed data available before evaluation."
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
if len(self.result.latentspaces._data) == 0:
|
|
933
|
+
raise ValueError(
|
|
934
|
+
"No latent spaces found in results. Please run predict() to "
|
|
935
|
+
"calculate embeddings before evaluation."
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
self.result = self.evaluator.evaluate(
|
|
939
|
+
datasets=self._datasets,
|
|
940
|
+
result=self.result,
|
|
941
|
+
ml_model_class=ml_model_class,
|
|
942
|
+
ml_model_regression=ml_model_regression,
|
|
943
|
+
params=params,
|
|
944
|
+
metric_class=metric_class,
|
|
945
|
+
metric_regression=metric_regression,
|
|
946
|
+
reference_methods=reference_methods,
|
|
947
|
+
split_type=split_type,
|
|
948
|
+
n_downsample=n_downsample,
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
_: Any = self.visualizer._plot_evaluation(result=self.result)
|
|
952
|
+
|
|
953
|
+
return self.result
|
|
954
|
+
|
|
955
|
+
def visualize(self, config: Optional[Union[None, DefaultConfig]] = None, **kwargs):
|
|
956
|
+
"""Creates visualizations of model results and performance.
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
config: Optional custom configuration for visualization.
|
|
960
|
+
**kwargs: Additional configuration parameters.
|
|
961
|
+
|
|
962
|
+
Raises:
|
|
963
|
+
NotImplementedError: If visualizer is not initialized.
|
|
964
|
+
"""
|
|
965
|
+
if self.visualizer is None:
|
|
966
|
+
raise NotImplementedError("Visualizer not initialized")
|
|
967
|
+
|
|
968
|
+
self.visualizer.visualize(result=self.result, config=self.config)
|
|
969
|
+
|
|
970
|
+
def show_result(self, split: str = "all", **kwargs):
|
|
971
|
+
"""Displays key visualizations of model results.
|
|
972
|
+
|
|
973
|
+
This method generates the following visualizations:
|
|
974
|
+
1. Loss Curves: Displays the absolute loss curves to provide insights into
|
|
975
|
+
the model's training and validation performance over epochs.
|
|
976
|
+
2. Latent Space Ridgeline Plot: Visualizes the distribution of the latent
|
|
977
|
+
space representations across different dimensions, offering a high-level
|
|
978
|
+
overview of the learned embeddings.
|
|
979
|
+
3. Latent Space 2D Scatter Plot: Projects the latent space into two dimensions
|
|
980
|
+
for a detailed view of the clustering or separation of data points.
|
|
981
|
+
|
|
982
|
+
These visualizations help in understanding the model's performance and
|
|
983
|
+
the structure of the latent space representations.
|
|
984
|
+
"""
|
|
985
|
+
print("Creating plots ...")
|
|
986
|
+
|
|
987
|
+
params: Optional[Union[List[str], str]] = kwargs.pop("params", None)
|
|
988
|
+
# Check if params are empty and annotation columns are available in config
|
|
989
|
+
if params is None and self.config.data_config.annotation_columns:
|
|
990
|
+
params = self.config.data_config.annotation_columns
|
|
991
|
+
|
|
992
|
+
if len(self.result.losses._data) != 0:
|
|
993
|
+
self.visualizer.show_loss(plot_type="absolute")
|
|
994
|
+
else:
|
|
995
|
+
warnings.warn(
|
|
996
|
+
"No loss data found in results. Skipping loss curve visualization."
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
if len(self.result.latentspaces._data) != 0:
|
|
1000
|
+
self.visualizer.show_latent_space(
|
|
1001
|
+
result=self.result, plot_type="Ridgeline", split=split, param=params
|
|
1002
|
+
)
|
|
1003
|
+
self.visualizer.show_latent_space(
|
|
1004
|
+
result=self.result, plot_type="2D-scatter", split=split, param=params
|
|
1005
|
+
)
|
|
1006
|
+
else:
|
|
1007
|
+
warnings.warn(
|
|
1008
|
+
"No latent spaces found in results. Please run predict() to "
|
|
1009
|
+
"calculate embeddings."
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
def run(
|
|
1013
|
+
self, data: Optional[Union[DatasetContainer, DataPackage]] = None
|
|
1014
|
+
) -> Result:
|
|
1015
|
+
"""Executes the complete pipeline from preprocessing to visualization.
|
|
1016
|
+
|
|
1017
|
+
Runs all pipeline steps in sequence and returns the result.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
data: Optional data for prediction (overrides test data).
|
|
1021
|
+
|
|
1022
|
+
Returns:
|
|
1023
|
+
Complete pipeline results.
|
|
1024
|
+
"""
|
|
1025
|
+
self.preprocess()
|
|
1026
|
+
self.fit()
|
|
1027
|
+
self.predict(data=data)
|
|
1028
|
+
self.visualize()
|
|
1029
|
+
return self.result
|
|
1030
|
+
|
|
1031
|
+
def save(self, file_path: str, save_all: bool = False):
|
|
1032
|
+
"""Saves the pipeline to a file.
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
file_path: Path where the pipeline should be saved.
|
|
1036
|
+
"""
|
|
1037
|
+
saver = Saver(file_path, save_all=save_all)
|
|
1038
|
+
saver.save(self)
|
|
1039
|
+
|
|
1040
|
+
@classmethod
|
|
1041
|
+
def load(cls, file_path) -> Any:
|
|
1042
|
+
"""Loads a pipeline from a file.
|
|
1043
|
+
|
|
1044
|
+
Args:
|
|
1045
|
+
file_path: Path to the saved pipeline.
|
|
1046
|
+
|
|
1047
|
+
Returns:
|
|
1048
|
+
The loaded pipeline instance.
|
|
1049
|
+
"""
|
|
1050
|
+
loader = Loader(file_path)
|
|
1051
|
+
return loader.load()
|
|
1052
|
+
|
|
1053
|
+
def sample_latent_space(
|
|
1054
|
+
self,
|
|
1055
|
+
n_samples: int,
|
|
1056
|
+
split: str = "test",
|
|
1057
|
+
epoch: int = -1,
|
|
1058
|
+
) -> torch.Tensor:
|
|
1059
|
+
"""Samples latent space points from the learned distribution.
|
|
1060
|
+
|
|
1061
|
+
If `n_samples` is not provided, this method returns one latent point per
|
|
1062
|
+
sample in the specified split (legacy behavior). If `n_samples` is given,
|
|
1063
|
+
it draws samples from the aggregated posterior distribution of the split.
|
|
1064
|
+
|
|
1065
|
+
Args:
|
|
1066
|
+
split: The split to sample from (train, valid, test), default is test.
|
|
1067
|
+
epoch: The epoch to sample from, default is the last epoch (-1).
|
|
1068
|
+
n_samples: Optional number of latent points to sample. If None,
|
|
1069
|
+
returns one latent point per available sample in the split.
|
|
1070
|
+
|
|
1071
|
+
Returns:
|
|
1072
|
+
z: torch.Tensor - The sampled latent space points.
|
|
1073
|
+
|
|
1074
|
+
Raises:
|
|
1075
|
+
ValueError: If the model has not been trained or latent statistics
|
|
1076
|
+
have not been computed.
|
|
1077
|
+
TypeError: If mu or logvar are not numpy arrays.
|
|
1078
|
+
"""
|
|
1079
|
+
|
|
1080
|
+
if not hasattr(self, "_trainer") or self._trainer is None:
|
|
1081
|
+
raise ValueError("Model is not trained yet. Please train the model first.")
|
|
1082
|
+
if self.result.mus is None or self.result.sigmas is None:
|
|
1083
|
+
raise ValueError("Model has not learned the latent space distribution yet.")
|
|
1084
|
+
if not isinstance(n_samples, int) or n_samples <= 0:
|
|
1085
|
+
raise ValueError("n_samples must be a positive integer.")
|
|
1086
|
+
|
|
1087
|
+
mu = self.result.mus.get(split=split, epoch=epoch)
|
|
1088
|
+
logvar = self.result.sigmas.get(split=split, epoch=epoch)
|
|
1089
|
+
|
|
1090
|
+
if not isinstance(mu, np.ndarray):
|
|
1091
|
+
raise TypeError(
|
|
1092
|
+
f"Expected value to be of type numpy.ndarray, got {type(mu)}."
|
|
1093
|
+
"This can happen if the model was not trained with VAE loss or if you forgot to run predict()"
|
|
1094
|
+
)
|
|
1095
|
+
if not isinstance(logvar, np.ndarray):
|
|
1096
|
+
raise TypeError(
|
|
1097
|
+
f"Expected value to be of type numpy.ndarray, got {type(logvar)}."
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
mu_t = torch.from_numpy(mu).to(
|
|
1101
|
+
device=self._trainer._model.device, dtype=self._trainer._model.dtype
|
|
1102
|
+
)
|
|
1103
|
+
logvar_t = torch.from_numpy(logvar).to(
|
|
1104
|
+
device=self._trainer._model.device, dtype=self._trainer._model.dtype
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
with torch.no_grad():
|
|
1108
|
+
global_mu = mu_t.mean(dim=0)
|
|
1109
|
+
global_logvar = logvar_t.mean(dim=0)
|
|
1110
|
+
|
|
1111
|
+
mu_exp = global_mu.expand(n_samples, -1)
|
|
1112
|
+
logvar_exp = global_logvar.expand(n_samples, -1)
|
|
1113
|
+
|
|
1114
|
+
z = self._trainer._model.reparameterize(mu_exp, logvar_exp)
|
|
1115
|
+
return z
|
|
1116
|
+
|
|
1117
|
+
def generate(
|
|
1118
|
+
self,
|
|
1119
|
+
n_samples: Optional[int] = None,
|
|
1120
|
+
latent_prior: Optional[Union[np.ndarray, torch.Tensor]] = None,
|
|
1121
|
+
split: str = "test",
|
|
1122
|
+
epoch: int = -1,
|
|
1123
|
+
) -> torch.Tensor:
|
|
1124
|
+
"""Generates new samples from the model's latent space.
|
|
1125
|
+
|
|
1126
|
+
This method allows for the generation of new data samples by sampling
|
|
1127
|
+
from the model's latent space. Users can either provide a custom latent
|
|
1128
|
+
prior or specify the number of samples to generate. If a custom latent
|
|
1129
|
+
prior is provided, its batch dimension must be compatible with n_samples.
|
|
1130
|
+
|
|
1131
|
+
Args:
|
|
1132
|
+
n_samples: The number of samples to generate.
|
|
1133
|
+
latent_prior: Optional custom latent prior distribution. If provided,
|
|
1134
|
+
this will be used for sampling instead of the learned distribution.
|
|
1135
|
+
The prior must either be a single latent vector or a batch of
|
|
1136
|
+
latent vectors matching n_samples.
|
|
1137
|
+
split: The split to sample from (train, valid, test), default is test.
|
|
1138
|
+
epoch: The epoch to sample from, default is the last epoch (-1).
|
|
1139
|
+
|
|
1140
|
+
Returns:
|
|
1141
|
+
torch.Tensor: The generated samples in the input space.
|
|
1142
|
+
|
|
1143
|
+
Raises:
|
|
1144
|
+
ValueError: If n_samples is not a positive integer or if the latent
|
|
1145
|
+
prior has incompatible dimensions.
|
|
1146
|
+
TypeError: If latent_prior is not a numpy array or tensor.
|
|
1147
|
+
"""
|
|
1148
|
+
if not isinstance(n_samples, int) or n_samples <= 0:
|
|
1149
|
+
if latent_prior is None:
|
|
1150
|
+
raise ValueError(
|
|
1151
|
+
"n_samples must be a positive integer or latent_prior provided."
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
if latent_prior is None:
|
|
1155
|
+
latent_prior = self.sample_latent_space(
|
|
1156
|
+
n_samples=n_samples, split=split, epoch=epoch
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
if isinstance(latent_prior, np.ndarray):
|
|
1160
|
+
latent_prior = torch.from_numpy(latent_prior).to(
|
|
1161
|
+
device=self._trainer._model.device,
|
|
1162
|
+
dtype=self._trainer._model.dtype,
|
|
1163
|
+
)
|
|
1164
|
+
if not isinstance(latent_prior, torch.Tensor):
|
|
1165
|
+
raise TypeError(
|
|
1166
|
+
f"latent_prior must be numpy.ndarray or torch.Tensor, got {type(latent_prior)}."
|
|
1167
|
+
)
|
|
1168
|
+
if not latent_prior.shape[1] == self.config.latent_dim:
|
|
1169
|
+
if self._trainer._model._mu.in_features == latent_prior.shape[1]:
|
|
1170
|
+
warnings.warn(
|
|
1171
|
+
f"latent_prior has latent dimension {latent_prior.shape[1]}, "
|
|
1172
|
+
"which matches the input feature dimension of the model. Did you "
|
|
1173
|
+
"mean to provide latent vectors of dimension "
|
|
1174
|
+
"For Ontix this is the default behaviour and the warning can be ignored. "
|
|
1175
|
+
f"{self.config.latent_dim}?"
|
|
1176
|
+
)
|
|
1177
|
+
else:
|
|
1178
|
+
raise ValueError(
|
|
1179
|
+
f"latent_prior has incompatible latent dimension {latent_prior.shape[1]}, "
|
|
1180
|
+
f"expected {self.config.latent_dim}."
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
with torch.no_grad():
|
|
1184
|
+
generated = self.decode(latent=latent_prior)
|
|
1185
|
+
return generated
|
|
1186
|
+
|
|
1187
|
+
def explain(
|
|
1188
|
+
self,
|
|
1189
|
+
method: Literal["DeepLiftShap", "IntegratedGradients"] = "DeepLiftShap",
|
|
1190
|
+
baseline_type: Literal["mean", "random_sample"] = "mean",
|
|
1191
|
+
baseline_group: str = "all",
|
|
1192
|
+
obs_col: str = None,
|
|
1193
|
+
n_subset: int = 100,
|
|
1194
|
+
seed_int: int = 12,
|
|
1195
|
+
split : Literal["train", "test","valid"] = "train",
|
|
1196
|
+
llm_explain: bool = False,
|
|
1197
|
+
llm_client: Literal["ollama", "mistral"] = "mistral",
|
|
1198
|
+
llm_model: str = "mistral-medium-latest",
|
|
1199
|
+
)-> pd.DataFrame:
|
|
1200
|
+
"""Runs the feature-importance explainer and returns gene-by-latent-dimension attribution scores.
|
|
1201
|
+
|
|
1202
|
+
Args:
|
|
1203
|
+
method: Specifies which attribution algorithm to use for explaining the model.
|
|
1204
|
+
baseline_type: Specifies whether the feature-importance algorithm should use the mean baseline or a random-sample baseline.
|
|
1205
|
+
baseline_group: Specifies whether the baseline is computed using all data (default) or which subset in obs_col to filter for.
|
|
1206
|
+
obs_col: If baseline_group is not 'all', this specifies the observation column used to filter the baseline data.
|
|
1207
|
+
n_subset: Specifies the number of cells to use when subsampling for the attribution computation.
|
|
1208
|
+
seed_int: Defines the random seed used for reproducible subsampling and attribution calculations.
|
|
1209
|
+
split: The split to use for feature importance calculation (train, valid, test), default is train.
|
|
1210
|
+
llm_explain:
|
|
1211
|
+
llm_client:
|
|
1212
|
+
llm_model:
|
|
1213
|
+
|
|
1214
|
+
Returns:
|
|
1215
|
+
pd.DataFrame: The generated samples in the input space.
|
|
1216
|
+
A DataFrame of attribution scores with genes as the index and latent dimensions as columns.
|
|
1217
|
+
Each entry represents the contribution of a given gene to a specific latent dimension.
|
|
1218
|
+
"""
|
|
1219
|
+
my_converter = AnnDataConverter()
|
|
1220
|
+
dataset: Optional[DatasetContainer] = get_dataset(self.result)
|
|
1221
|
+
if dataset is None:
|
|
1222
|
+
raise ValueError(
|
|
1223
|
+
"No dataset available for explanation."
|
|
1224
|
+
"This happens if you used .save and .load, and did not run .predict before."
|
|
1225
|
+
"This can also happen if you run .explain before .preprocess or .fit."
|
|
1226
|
+
)
|
|
1227
|
+
adata_ACX: Optional[Dict[str, ad.AnnData]] = my_converter.dataset_to_adata(
|
|
1228
|
+
dataset, split=split
|
|
1229
|
+
)
|
|
1230
|
+
'''
|
|
1231
|
+
adata_test: Optional[Dict[str, ad.AnnData]] = my_converter.dataset_to_adata(
|
|
1232
|
+
dataset, split="test"
|
|
1233
|
+
)
|
|
1234
|
+
adata_valid: Optional[Dict[str, ad.AnnData]] = my_converter.dataset_to_adata(
|
|
1235
|
+
dataset, split="valid"
|
|
1236
|
+
)
|
|
1237
|
+
'''
|
|
1238
|
+
model = self.result.model
|
|
1239
|
+
if model is None:
|
|
1240
|
+
raise ValueError(
|
|
1241
|
+
"No model available for explanation."
|
|
1242
|
+
"This happens if you used .save and .load, and did not run .fit before."
|
|
1243
|
+
"This can also happen if you run .explain before .fit."
|
|
1244
|
+
)
|
|
1245
|
+
explainer = FeatureImportanceExplainer(adata_ACX=adata_ACX['global'],
|
|
1246
|
+
model = model,
|
|
1247
|
+
n_subset = n_subset,
|
|
1248
|
+
method=method,
|
|
1249
|
+
baseline_group = baseline_group,
|
|
1250
|
+
obs_col = obs_col,
|
|
1251
|
+
baseline_type = baseline_type,
|
|
1252
|
+
seed_int = seed_int,
|
|
1253
|
+
)
|
|
1254
|
+
#return adata_train
|
|
1255
|
+
df_attributions = explainer.explain()
|
|
1256
|
+
return df_attributions
|
|
1257
|
+
# also note tha adata_<split> can be None, if the split is not available
|
|
1258
|
+
# so best to check this before concatenating or using them
|
|
1259
|
+
|
|
1260
|
+
if llm_explain:
|
|
1261
|
+
# TODO Vincent:
|
|
1262
|
+
# Je nachdem wie die Gene Liste aussieht, müsstet du noch in src/autoencodix/utils/_llm_explainer.py
|
|
1263
|
+
# in _init_prompt anpassen, wie der prompt gebaut wird. Ich gehe jetzt von einer Liste aus String aus, aber
|
|
1264
|
+
# ich wusste nicht genau was dein return Typ ist.
|
|
1265
|
+
|
|
1266
|
+
llm_explainer = LLMExplainer(
|
|
1267
|
+
client_name=llm_client,
|
|
1268
|
+
model_name=llm_model,
|
|
1269
|
+
gene_list=["GeneA", "GeneB", "GeneC"], # Example gene list
|
|
1270
|
+
)
|
|
1271
|
+
explanation = llm_explainer.explain()
|
|
1272
|
+
print("LLM Explanation:")
|
|
1273
|
+
print(explanation)
|
|
1274
|
+
return explanation
|