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,77 @@
1
+ import torch
2
+ from typing import Optional, Type, Union, Tuple, Dict, Any
3
+ from autoencodix.trainers._general_trainer import GeneralTrainer
4
+ from autoencodix.base._base_dataset import BaseDataset
5
+ from autoencodix.base._base_loss import BaseLoss
6
+ from autoencodix.base._base_autoencoder import BaseAutoencoder
7
+ from autoencodix.utils._result import Result
8
+ from autoencodix.configs.default_config import DefaultConfig
9
+
10
+
11
+ class OntixTrainer(GeneralTrainer):
12
+ """Specialized trainer for Ontix (ontology-based) autoencoders.
13
+
14
+ Handles ontology-specific weight masking and positive weight constraints. Uses most of the
15
+ functionality from the GeneralTrainer class and ontology-specific functionality is added via a hook
16
+ after the backward pass.
17
+
18
+ Attributes:
19
+ Inherits all attributes from GeneralTrainer.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ trainset: Optional[BaseDataset],
25
+ validset: Optional[BaseDataset],
26
+ result: Result,
27
+ config: DefaultConfig,
28
+ model_type: Type[BaseAutoencoder],
29
+ loss_type: Type[BaseLoss],
30
+ ontologies: Optional[Union[Tuple, Dict[Any, Any]]],
31
+ **kwargs,
32
+ ):
33
+ """Initializes the OntixTrainer with the given datasets, model, and configuration.
34
+
35
+
36
+ Args:
37
+ trainset: The dataset used for training.
38
+ validset: The dataset used for validation, if provided.
39
+ result: An object to store and manage training results.
40
+ config: Configuration object containing training hyperparameters and settings.
41
+ model_type: The autoencoder model class to be trained.
42
+ loss_type: The loss function class specific to the model.
43
+ ontologies: Ontology information required for Ontix.
44
+ """
45
+ super().__init__(
46
+ trainset=trainset,
47
+ validset=validset,
48
+ result=result,
49
+ config=config,
50
+ model_type=model_type,
51
+ loss_type=loss_type,
52
+ ontologies=ontologies,
53
+ )
54
+
55
+ def _ontix_hook(self):
56
+ """Apply ontology-specific processing after backward pass."""
57
+ # Apply positive weight constraint to decoder
58
+ self._model._decoder.apply(self._model._positive_dec)
59
+
60
+ # Apply ontology-based weight masking
61
+ with torch.no_grad():
62
+ self._validate_masks()
63
+ self._apply_weight_masks()
64
+
65
+ def _validate_masks(self):
66
+ """Validate that number of masks matches decoder layers."""
67
+ if len(self._model.masks) != len(self._model._decoder):
68
+ raise ValueError(
69
+ f"Number of masks ({len(self._model.masks)}) does not match "
70
+ f"number of decoder layers ({len(self._model._decoder)})"
71
+ )
72
+
73
+ def _apply_weight_masks(self):
74
+ """Apply weight masks to decoder layers."""
75
+ for i, mask in enumerate(self._model.masks):
76
+ mask = mask.to(self._fabric.device)
77
+ self._model._decoder[i].weight.mul_(mask)
@@ -0,0 +1,522 @@
1
+ import os
2
+ import pickle
3
+ from typing import Dict, List, Optional, Tuple, Type, Any
4
+
5
+ import lightning_fabric
6
+ import numpy as np
7
+ import pandas as pd
8
+ import torch
9
+
10
+ from autoencodix.base._base_autoencoder import BaseAutoencoder
11
+ from autoencodix.base._base_dataset import BaseDataset
12
+ from autoencodix.base._base_loss import BaseLoss
13
+ from autoencodix.data._numeric_dataset import NumericDataset
14
+ from autoencodix.data._multimodal_dataset import MultiModalDataset
15
+ from autoencodix.trainers._general_trainer import GeneralTrainer
16
+ from autoencodix.utils._result import Result
17
+ from autoencodix.configs.default_config import DefaultConfig
18
+
19
+
20
+ class StackixOrchestrator:
21
+ """StackixOrchestrator coordinates the training of multi-modality VAE stacking.
22
+
23
+ This orchestrator manages both parallel and sequential training of modality-specific
24
+ autoencoders, followed by creating a concatenated latent space for the final
25
+ stacked model training. It leverages Lightning Fabric's distribution strategies
26
+ for efficient training.
27
+
28
+ Attributes:
29
+ _workdir: Directory for saving intermediate models and results
30
+ modality_models: Dictionary of trained models for each modality
31
+ modality_results: Dictionary of training results for each modality
32
+ _modality_latent_dims: Dictionary of latent dimensions for each modality
33
+ concatenated_latent_spaces: Dictionary of concatenated latent spaces by split
34
+ _dataset_type: Class to use for creating datasets
35
+ _fabric: Lightning Fabric instance for distributed operations
36
+ _trainer_class: Class to use for training (dependency injection)
37
+ trainset: Training dataset containing multiple modalities
38
+ validset: Validation dataset containing multiple modalities
39
+ testset: Test dataset containing multiple modalities
40
+ loss_type: Type of loss function to use for training
41
+ model_type: Type of autoencoder model to use for each modality
42
+ config: Configuration parameters for training and model architecture
43
+ stacked_model: The final stacked autoencoder model (initialized later)
44
+ stacked_trainer: Trainer for the stacked model (initialized later)
45
+ concat_idx: Dictionary tracking the start and end indices of each modality in the concatenated latent space
46
+ dropped_indices_map: Dictionary tracking dropped sample indices for each modality during alignment
47
+ reconstruction_shapes: Dictionary storing original shapes of latent spaces for reconstruction
48
+ common_sample_ids: Common sample IDs across all modalities for alignment
49
+
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ trainset: Optional[MultiModalDataset],
55
+ validset: Optional[MultiModalDataset],
56
+ config: DefaultConfig,
57
+ model_type: Type[BaseAutoencoder],
58
+ loss_type: Type[BaseLoss],
59
+ testset: Optional[MultiModalDataset] = None,
60
+ trainer_type: Type[GeneralTrainer] = GeneralTrainer,
61
+ dataset_type: Type[BaseDataset] = NumericDataset,
62
+ workdir: str = "./stackix_work",
63
+ ):
64
+ """
65
+ Initialize the StackixOrchestrator with datasets and configuration.
66
+
67
+ Args:
68
+ trainset: Training dataset containing multiple modalities
69
+ validset: Validation dataset containing multiple modalities
70
+ config: Configuration parameters for training and model architecture
71
+ model_type: Type of autoencoder model to use for each modality
72
+ loss_type: Type of loss function to use for training
73
+ testset: Dataset with test split
74
+ trainer_type: Type to use for training (default is GeneralTrainer)
75
+ dataset_type: Type to use for creating datasets (default is NumericDataset)
76
+ workdir: Directory to save intermediate models and results (default is "./stackix_work")
77
+ """
78
+ self._trainset = trainset
79
+ self._validset = validset
80
+ self._testset = testset
81
+ self._config = config
82
+ self._model_type = model_type
83
+ self._loss_type = loss_type
84
+ self._workdir = workdir
85
+ self._trainer_class = trainer_type
86
+ self._dataset_type = dataset_type
87
+
88
+ # Initialize fabric for distributed operations
89
+ strategy = "auto"
90
+ if hasattr(config, "gpu_strategy"):
91
+ strategy = config.gpu_strategy
92
+
93
+ self._fabric = lightning_fabric.Fabric(
94
+ accelerator=(
95
+ str(config.device) if hasattr(config, "device") else None
96
+ ), # ty: ignore[invalid-argument-type]
97
+ devices=config.n_gpus if hasattr(config, "n_gpus") else 1,
98
+ precision=(
99
+ config.float_precision if hasattr(config, "float_precision") else "32"
100
+ ),
101
+ strategy=strategy,
102
+ )
103
+
104
+ # Initialize storage for models and results
105
+ self.modality_trainers: Dict[str, BaseAutoencoder] = {}
106
+ self.modality_results: Dict[str, Result] = {}
107
+ self._modality_latent_dims: Dict[str, int] = {}
108
+ self.concatenated_latent_spaces: Dict[str, torch.Tensor] = {}
109
+
110
+ # Stacked model and trainer will be created during the process
111
+ self.stacked_model: Optional[BaseAutoencoder] = None
112
+ self.stacked_trainer: Optional[GeneralTrainer] = None
113
+ self.concat_idx: Dict[str, Optional[Tuple[int, int]]] = {}
114
+ self.dropped_indices_map: Dict[str, np.ndarray] = {}
115
+ self.reconstruction_shapes: Dict[str, torch.Size] = {}
116
+ self.common_sample_ids: Optional[pd.Index] = None
117
+
118
+ # Create the directory if it doesn't exist
119
+ os.makedirs(name=self._workdir, exist_ok=True)
120
+
121
+ def set_testset(self, testset: MultiModalDataset) -> None:
122
+ """Set the test dataset for the orchestrator.
123
+
124
+ Args:
125
+ testset: Test dataset containing multiple modalities
126
+ """
127
+ self._testset = testset
128
+
129
+ def _train_modality(
130
+ self,
131
+ modality: str,
132
+ modality_dataset: BaseDataset,
133
+ valid_modality_dataset: Optional[BaseDataset] = None,
134
+ ) -> None:
135
+ """Trains a single modality and returns the trained model and result.
136
+
137
+ This function is designed to be executed within a Lightning Fabric context.
138
+ It trains an individual modality model and saves the results to disk.
139
+
140
+ Args:
141
+ modality: Modality name/identifier
142
+ modality_dataset: Training dataset for this modality
143
+ valid_modality_dataset: Validation dataset for this modality (default is None)
144
+
145
+ Returns:
146
+ Trained model and result object
147
+ """
148
+ print(f"Training modality: {modality}")
149
+ result = Result()
150
+ trainer = self._trainer_class(
151
+ trainset=modality_dataset,
152
+ validset=valid_modality_dataset,
153
+ result=result,
154
+ config=self._config,
155
+ model_type=self._model_type,
156
+ loss_type=self._loss_type,
157
+ )
158
+
159
+ result = trainer.train()
160
+ self.modality_results[modality] = result
161
+ self.modality_trainers[modality] = trainer
162
+
163
+ def _train_distributed(self, keys: List[str]) -> None:
164
+ """Trains modality models in a distributed fashion using Lightning Fabric.
165
+
166
+ Uses Lightning Fabric's built-in capabilities for distributing work across devices.
167
+ Each process trains a subset of modalities, then loads results from other processes.
168
+
169
+ Args:
170
+ keys: List of modality keys to train
171
+ """
172
+ # For parallel training with multiple devices
173
+ strategy = (
174
+ self._config.gpu_strategy
175
+ if hasattr(self._config, "gpu_strategy")
176
+ else "auto"
177
+ )
178
+ n_gpus = self._config.n_gpus if hasattr(self._config, "n_gpus") else 1
179
+ device: str = self._config.device
180
+
181
+ if strategy == "auto" and device in ["cuda", "gpu"]:
182
+ strategy = "ddp" if n_gpus > 1 else "dp"
183
+
184
+ # Create a fabric instance with appropriate parallelism strategy
185
+ fabric = lightning_fabric.Fabric(
186
+ accelerator=device,
187
+ devices=min(n_gpus, len(keys)), # No more devices than modalities
188
+ precision=(
189
+ self._config.float_precision
190
+ if hasattr(self._config, "float_precision")
191
+ else "32"
192
+ ),
193
+ strategy=strategy,
194
+ )
195
+
196
+ # Launch distributed training
197
+ fabric.launch()
198
+
199
+ # Device ID within the process group
200
+ local_rank = fabric.local_rank if hasattr(fabric, "local_rank") else 0
201
+ world_size = fabric.world_size if hasattr(fabric, "world_size") else 1
202
+
203
+ # Distribute modalities across ranks
204
+ my_modalities = [k for i, k in enumerate(keys) if i % world_size == local_rank]
205
+
206
+ # Train assigned modalities
207
+ for modality in my_modalities:
208
+ train_dataset = self._trainset.datasets[modality]
209
+ valid_dataset = (
210
+ self._validset.datasets.get(modality)
211
+ if self._validset and hasattr(self._validset, "datasets")
212
+ else None
213
+ )
214
+
215
+ self._train_modality(
216
+ modality=modality,
217
+ modality_dataset=train_dataset,
218
+ valid_modality_dataset=valid_dataset,
219
+ )
220
+ self._modality_latent_dims[modality] = train_dataset.get_input_dim()
221
+
222
+ # Synchronize across processes to ensure all modalities are trained
223
+ if world_size > 1:
224
+ fabric.barrier()
225
+
226
+ # Load models and results from other processes
227
+ for modality in keys:
228
+ if modality in self.modality_trainers.keys():
229
+ continue
230
+ # Load model state dict
231
+ model_path = os.path.join(self._workdir, f"{modality}_model.ckpt")
232
+
233
+ # Get input dimension for this modality
234
+ input_dim = self._trainset.datasets[modality].get_input_dim()
235
+
236
+ # Initialize a new model
237
+ model = self._model_type(config=self._config, input_dim=input_dim)
238
+ model.load_state_dict(
239
+ state_dict=torch.load(f=model_path, map_location="cpu")
240
+ )
241
+
242
+ # Load result
243
+ with open(
244
+ file=os.path.join(self._workdir, f"{modality}_result.pkl"),
245
+ mode="rb",
246
+ ) as f:
247
+ result = pickle.load(file=f)
248
+
249
+ self.modality_trainers[modality] = model
250
+ self.modality_results[modality] = result
251
+ self._modality_latent_dims[modality] = input_dim
252
+
253
+ def _train_sequential(self, keys: List[str]) -> None:
254
+ """Trains modality models sequentially on a single device.
255
+
256
+ Used when distributed training is not available or not necessary.
257
+ Processes each modality one after another.
258
+
259
+ Args:
260
+ keys: List of modality keys to train
261
+ """
262
+ for modality in keys:
263
+ print(f"Training modality: {modality}")
264
+ train_dataset = self._trainset.datasets[modality]
265
+ valid_dataset = (
266
+ self._validset.datasets.get(modality) if self._validset else None
267
+ )
268
+
269
+ self._train_modality(
270
+ modality=modality,
271
+ modality_dataset=train_dataset,
272
+ valid_modality_dataset=valid_dataset,
273
+ )
274
+
275
+ self._modality_latent_dims[modality] = train_dataset.get_input_dim()
276
+
277
+ def _extract_latent_spaces(
278
+ self, result_dict: Dict[str, Any], split: str = "train"
279
+ ) -> torch.Tensor:
280
+ """Extracts, aligns, and concatenates latent spaces, populating instance attributes for later reconstruction.
281
+
282
+ Args:
283
+ result_dict: Dictionary of Result objects from modality training
284
+ split: Data split to extract latent spaces from ("train", "valid", "test"), default is "train"
285
+ """
286
+ # --- Step 1: Extract Latent Spaces and Sample IDs ---
287
+ all_latents = {}
288
+ all_sample_ids = {}
289
+ for name, result in result_dict.items():
290
+ try:
291
+ latent_space = result.latentspaces.get(split=split, epoch=-1)
292
+ sample_ids = result.sample_ids.get(split=split, epoch=-1)
293
+
294
+ if latent_space.shape[0] != len(sample_ids):
295
+ raise ValueError(
296
+ f"Mismatch between latent space rows ({latent_space.shape[0]}) and sample IDs ({len(sample_ids)}) for modality '{name}'."
297
+ )
298
+
299
+ all_latents[name] = latent_space
300
+ all_sample_ids[name] = sample_ids
301
+ self.reconstruction_shapes[name] = latent_space.shape
302
+ except Exception as e:
303
+ raise ValueError(
304
+ f"Failed to extract data for modality {name} on split {split}: {e}"
305
+ )
306
+
307
+ if not all_latents:
308
+ raise ValueError("No latent spaces were extracted.")
309
+
310
+ # --- Step 2: Find Common Sample IDs ---
311
+ initial_ids = set(next(iter(all_sample_ids.values())))
312
+ common_ids_set = initial_ids.intersection(
313
+ *[set(ids) for ids in all_sample_ids.values()]
314
+ )
315
+
316
+ if not common_ids_set:
317
+ raise ValueError(
318
+ "No common samples found across all modalities for concatenation."
319
+ )
320
+ # common_ids = [cid for cid in list(common_ids_set) if cid]
321
+ self.common_sample_ids = pd.Index(sorted(list(common_ids_set)))
322
+
323
+ # self.common_sample_ids = pd.Index(sorted(list(common_ids_set)))
324
+ print(
325
+ f"Found {len(self.common_sample_ids)} common samples for the stacked autoencoder."
326
+ )
327
+
328
+ # --- Step 3 & 4: Align Latent Spaces and Track Dropped Indices ---
329
+ aligned_latents_list = []
330
+ start_idx = 0
331
+ for name, latent_space in all_latents.items():
332
+ original_ids = pd.Index(all_sample_ids[name])
333
+ latent_df = pd.DataFrame(latent_space, index=original_ids)
334
+
335
+ aligned_df = latent_df.loc[self.common_sample_ids]
336
+ aligned_latents_list.append(torch.from_numpy(aligned_df.values))
337
+
338
+ end_idx = start_idx + aligned_df.shape[1]
339
+ self.concat_idx[name] = (start_idx, end_idx)
340
+ start_idx = end_idx
341
+
342
+ dropped_mask = ~original_ids.isin(self.common_sample_ids)
343
+ self.dropped_indices_map[name] = np.where(dropped_mask)[0]
344
+
345
+ # --- Step 5: Concatenate ---
346
+ stackix_input = torch.cat(aligned_latents_list, dim=1)
347
+ return stackix_input
348
+
349
+ def train_modalities(self) -> Tuple[Dict[str, BaseAutoencoder], Dict[str, Result]]:
350
+ """Trains all modality-specific models.
351
+
352
+ This is the first phase of Stackix training where each modality is
353
+ trained independently before their latent spaces are combined.
354
+
355
+ Returns:
356
+ Dictionary of trained models for each modality and Dictionary of training results
357
+ for each modality.
358
+
359
+ Raises:
360
+ ValueError: If trainset is not a MultiModalDataset or has no modalities
361
+ """
362
+ # if not isinstance(self._trainset, MulDataset):
363
+ # raise ValueError("Trainset must be a MultiModalDataset for Stackix training")
364
+
365
+ keys = self._trainset.datasets.keys()
366
+ if not keys:
367
+ raise ValueError("No modalities found in trainset")
368
+
369
+ n_modalities = len(keys)
370
+
371
+ # Determine if we should use distributed training
372
+ n_gpus = self._config.n_gpus if hasattr(self._config, "n_gpus") else 0
373
+ use_distributed = bool(n_gpus and n_gpus > 1 and n_modalities > 1)
374
+
375
+ if use_distributed:
376
+ self._train_distributed(keys=keys)
377
+ else:
378
+ self._train_sequential(keys=keys)
379
+
380
+ return self.modality_trainers, self.modality_results
381
+
382
+ def prepare_latent_datasets(self, split: str) -> NumericDataset:
383
+ """Prepares datasets with concatenated latent spaces for stacked model training.
384
+
385
+ This is the second phase of Stackix training where latent spaces from
386
+ all modalities are extracted and concatenated.
387
+
388
+ Returns:
389
+ Training and validation datasets with concatenated latent spaces
390
+
391
+ Raises:
392
+ ValueError: If no modality models have been trained or no latent spaces could be extracted
393
+ """
394
+ if not self.modality_trainers:
395
+ raise ValueError(
396
+ "No modality models have been trained. Call train_modalities() first."
397
+ )
398
+
399
+ # Extract and concatenate latent spaces
400
+ if split == "test":
401
+ if self._testset is None:
402
+ raise ValueError(
403
+ "No test dataset available. Please provide a test dataset for prediction."
404
+ )
405
+ latent = self._extract_latent_spaces(
406
+ result_dict=self.predict_modalities(data=self._testset), split=split
407
+ )
408
+ ds = NumericDataset(
409
+ data=latent,
410
+ config=self._config,
411
+ sample_ids=self.common_sample_ids,
412
+ feature_ids=[
413
+ f"{k}_latent_{i}"
414
+ for k, (start, end) in self.concat_idx.items()
415
+ for i in range(start, end)
416
+ ],
417
+ )
418
+ return ds
419
+
420
+ latent = self._extract_latent_spaces(
421
+ result_dict=self.modality_results, split=split
422
+ )
423
+ # Create datasets for the concatenated latent spaces
424
+ feature_ids = [
425
+ f"{k}_latent_{i}"
426
+ for k, (start, end) in self.concat_idx.items()
427
+ for i in range(start, end)
428
+ ]
429
+ ds = NumericDataset(
430
+ data=latent,
431
+ config=self._config,
432
+ sample_ids=self.common_sample_ids,
433
+ feature_ids=feature_ids,
434
+ )
435
+
436
+ return ds
437
+
438
+ def predict_modalities(self, data: MultiModalDataset) -> Dict[str, torch.Tensor]:
439
+ """Predicts using the trained models for each modality.
440
+
441
+ Args:
442
+ data: Input data for prediction, uses test data if not provided
443
+
444
+ Returns:
445
+ Dictionary of reconstructed tensors by modality
446
+
447
+ Raises:
448
+ ValueError: If model has not been trained yet or no data is available
449
+ """
450
+ if not self.modality_trainers:
451
+ raise ValueError(
452
+ "No modality models have been trained. Call train_modalities() first."
453
+ )
454
+
455
+ predictions = {}
456
+ for modality, trainer in self.modality_trainers.items():
457
+ predictions[modality] = trainer.predict(
458
+ data=data.datasets[modality],
459
+ model=trainer._model,
460
+ )
461
+ return predictions
462
+
463
+ def reconstruct_from_stack(
464
+ self, reconstructed_stack: torch.Tensor
465
+ ) -> Dict[str, torch.Tensor]:
466
+ """Reconstructs the full data for each modality from the stacked latent reconstruction.
467
+
468
+ Args:
469
+ reconstructed_stack: Tensor with the reconstructed concatenated latent space
470
+ Returns:
471
+ Dictionary of reconstructed tensors by modality
472
+ """
473
+ modality_reconstructions: Dict[str, torch.Tensor] = {}
474
+
475
+ for trainer in self.modality_trainers.values():
476
+ trainer._model.eval()
477
+
478
+ for name, (start_idx, end_idx) in self.concat_idx.items():
479
+ # =================================================================
480
+ # STEP 1: DE-CONCATENATE THE ALIGNED PART
481
+ # This is the small, dense reconstruction matching the common samples.
482
+ # =================================================================
483
+ aligned_latent_recon = reconstructed_stack[:, start_idx:end_idx]
484
+
485
+ # =================================================================
486
+ # STEP 2: RE-ASSEMBLE THE FULL-SIZE LATENT SPACE
487
+ # This is the logic from the old `reconstruct_full_latent` function,
488
+ # now integrated directly here.
489
+ # =================================================================
490
+ original_shape = self.reconstruction_shapes[name]
491
+ dropped_indices = self.dropped_indices_map[name]
492
+ n_original_samples = original_shape[0]
493
+
494
+ # Determine the original integer positions of the samples that were KEPT.
495
+ kept_indices = np.delete(np.arange(n_original_samples), dropped_indices)
496
+
497
+ # Create a full-size placeholder tensor matching the original latent space.
498
+ # We use zeros, as they are a neutral input for most decoders.
499
+ full_latent_recon = torch.zeros(
500
+ size=original_shape,
501
+ dtype=aligned_latent_recon.dtype,
502
+ device=self._fabric.device,
503
+ )
504
+
505
+ # Place the aligned reconstruction data into the correct rows of the full tensor.
506
+ full_latent_recon[kept_indices, :] = self._fabric.to_device(
507
+ aligned_latent_recon
508
+ )
509
+
510
+ # =================================================================
511
+ # STEP 3: DECODE THE FULL-SIZE LATENT TENSOR
512
+ # The decoder now receives a tensor with the correct number of samples,
513
+ # including the rows of zeros for the dropped samples.
514
+ # =================================================================
515
+ with torch.no_grad():
516
+ model = self.modality_trainers[name].get_model()
517
+ model = self._fabric.to_device(model)
518
+
519
+ final_recon = model.decode(full_latent_recon)
520
+ modality_reconstructions[name] = final_recon.cpu()
521
+
522
+ return modality_reconstructions