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.

Files changed (91) hide show
  1. autoencodix/__init__.py +27 -0
  2. autoencodix/base/.ipynb_checkpoints/_base_pipeline-checkpoint.py +1274 -0
  3. autoencodix/base/.ipynb_checkpoints/_base_trainer-checkpoint.py +247 -0
  4. autoencodix/base/__init__.py +21 -0
  5. autoencodix/base/_base_autoencoder.py +138 -0
  6. autoencodix/base/_base_dataset.py +103 -0
  7. autoencodix/base/_base_evaluator.py +34 -0
  8. autoencodix/base/_base_loss.py +311 -0
  9. autoencodix/base/_base_pipeline.py +1355 -0
  10. autoencodix/base/_base_predictor.py +6 -0
  11. autoencodix/base/_base_preprocessor.py +1033 -0
  12. autoencodix/base/_base_trainer.py +259 -0
  13. autoencodix/base/_base_visualizer.py +578 -0
  14. autoencodix/base/sampleconfig.yaml +132 -0
  15. autoencodix/configs/__init__.py +21 -0
  16. autoencodix/configs/default_config.py +652 -0
  17. autoencodix/configs/disentanglix_config.py +15 -0
  18. autoencodix/configs/maskix_config.py +39 -0
  19. autoencodix/configs/ontix_config.py +35 -0
  20. autoencodix/configs/stackix_config.py +33 -0
  21. autoencodix/configs/vanillix_config.py +18 -0
  22. autoencodix/configs/varix_config.py +15 -0
  23. autoencodix/configs/xmodalix_config.py +43 -0
  24. autoencodix/disentanglix.py +72 -0
  25. autoencodix/evaluate/.ipynb_checkpoints/evaluate-checkpoint.py +532 -0
  26. autoencodix/evaluate/__init__.py +4 -0
  27. autoencodix/evaluate/_general_evaluator.py +730 -0
  28. autoencodix/evaluate/_xmodalix_evaluator.py +376 -0
  29. autoencodix/imagix.py +76 -0
  30. autoencodix/losses/disentanglix_loss.py +215 -0
  31. autoencodix/losses/maskix_loss.py +94 -0
  32. autoencodix/losses/vanillix_loss.py +28 -0
  33. autoencodix/losses/varix_loss.py +102 -0
  34. autoencodix/losses/xmodal_loss.py +304 -0
  35. autoencodix/maskix.py +108 -0
  36. autoencodix/modeling/.ipynb_checkpoints/__init__-checkpoint.py +16 -0
  37. autoencodix/modeling/.ipynb_checkpoints/_captum_forward-checkpoint.py +16 -0
  38. autoencodix/modeling/__init__.py +20 -0
  39. autoencodix/modeling/_captum_forward.py +20 -0
  40. autoencodix/modeling/_classifier.py +45 -0
  41. autoencodix/modeling/_imagevae_architecture.py +296 -0
  42. autoencodix/modeling/_imgfast_architecture.py +288 -0
  43. autoencodix/modeling/_layer_factory.py +71 -0
  44. autoencodix/modeling/_maskix_architecture.py +190 -0
  45. autoencodix/modeling/_ontix_architecture.py +305 -0
  46. autoencodix/modeling/_vanillix_architecture.py +136 -0
  47. autoencodix/modeling/_varix_architecture.py +192 -0
  48. autoencodix/notebooks/__init__.py +0 -0
  49. autoencodix/ontix.py +136 -0
  50. autoencodix/py.typed +0 -0
  51. autoencodix/stackix.py +122 -0
  52. autoencodix/trainers/__init__.py +6 -0
  53. autoencodix/trainers/_general_trainer.py +525 -0
  54. autoencodix/trainers/_maskix_trainer.py +116 -0
  55. autoencodix/trainers/_ontix_trainer.py +77 -0
  56. autoencodix/trainers/_stackix_orchestrator.py +522 -0
  57. autoencodix/trainers/_stackix_trainer.py +209 -0
  58. autoencodix/trainers/_xmodal_trainer.py +1006 -0
  59. autoencodix/utils/.ipynb_checkpoints/_explainer-checkpoint.py +118 -0
  60. autoencodix/utils/.ipynb_checkpoints/_model_output-checkpoint.py +30 -0
  61. autoencodix/utils/.ipynb_checkpoints/_utils-checkpoint.py +626 -0
  62. autoencodix/utils/.ipynb_checkpoints/adata_converter-checkpoint.py +99 -0
  63. autoencodix/utils/.ipynb_checkpoints/feature_importance-checkpoint.py +823 -0
  64. autoencodix/utils/__init__.py +22 -0
  65. autoencodix/utils/_annealer.py +193 -0
  66. autoencodix/utils/_bulkreader.py +232 -0
  67. autoencodix/utils/_explainer.py +246 -0
  68. autoencodix/utils/_imgreader.py +404 -0
  69. autoencodix/utils/_internals.py +23 -0
  70. autoencodix/utils/_llm_explainer.py +318 -0
  71. autoencodix/utils/_losses.py +20 -0
  72. autoencodix/utils/_model_output.py +30 -0
  73. autoencodix/utils/_result.py +393 -0
  74. autoencodix/utils/_screader.py +76 -0
  75. autoencodix/utils/_traindynamics.py +187 -0
  76. autoencodix/utils/_utils.py +690 -0
  77. autoencodix/utils/adata_converter.py +111 -0
  78. autoencodix/utils/example_data.py +398 -0
  79. autoencodix/utils/feature_importance.py +829 -0
  80. autoencodix/utils/prompts.py +21 -0
  81. autoencodix/vanillix.py +141 -0
  82. autoencodix/varix.py +80 -0
  83. autoencodix/visualize/__init__.py +5 -0
  84. autoencodix/visualize/_general_visualizer.py +913 -0
  85. autoencodix/visualize/_imagix_visualizer.py +111 -0
  86. autoencodix/visualize/_xmodal_visualizer.py +841 -0
  87. autoencodix/visualize/visualize.py +1214 -0
  88. autoencodix/xmodalix.py +318 -0
  89. autoencodix-0.2.0.dist-info/METADATA +143 -0
  90. autoencodix-0.2.0.dist-info/RECORD +91 -0
  91. 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