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,578 @@
1
+ import abc
2
+ import os
3
+ from typing import Optional, Union
4
+ import pandas as pd
5
+ import matplotlib
6
+ from matplotlib import pyplot as plt
7
+ import seaborn as sns # type: ignore
8
+ import seaborn.objects as so
9
+ import torch
10
+ import warnings
11
+
12
+ from autoencodix.utils._result import Result
13
+ from autoencodix.utils._utils import nested_dict, nested_to_tuple, show_figure
14
+ from autoencodix.configs.default_config import DefaultConfig
15
+
16
+
17
+ class BaseVisualizer(abc.ABC):
18
+ """Defines the interface for visualizing training results.
19
+
20
+ Attributes:
21
+ plots: A nested dictionary to store various plots.
22
+ """
23
+
24
+ def __init__(self):
25
+ self.plots = nested_dict()
26
+
27
+ def __setitem__(self, key, elem):
28
+ self.plots[key] = elem
29
+
30
+ ### Abstract Methods ###
31
+ @abc.abstractmethod
32
+ def visualize(self, result: Result, config: DefaultConfig) -> Result:
33
+ pass
34
+
35
+ @abc.abstractmethod
36
+ def show_latent_space(
37
+ self,
38
+ result: Result,
39
+ plot_type: str = "2D-scatter",
40
+ labels: Optional[Union[list, pd.Series, None]] = None,
41
+ param: Optional[Union[list, str]] = None,
42
+ epoch: Optional[Union[int, None]] = None,
43
+ split: str = "all",
44
+ ) -> None:
45
+ pass
46
+
47
+ @abc.abstractmethod
48
+ def show_weights(self) -> None:
49
+ pass
50
+
51
+ ### General Functions used by all Visualizers in similar way ###
52
+
53
+ def show_loss(self, plot_type: str = "absolute") -> None:
54
+ """
55
+ Display the loss plot.
56
+ Args:
57
+ plot_type: Type of loss plot to display. Options are "absolute" or "relative". Options are
58
+ "absolute" for the absolute loss plot and
59
+ "relative" for the relative loss plot.
60
+ Defaults to "absolute".
61
+ Returns:
62
+ None
63
+ """
64
+ if plot_type == "absolute":
65
+ if "loss_absolute" not in self.plots.keys():
66
+ print("Absolute loss plot not found in the plots dictionary")
67
+ print(
68
+ "This happens, when you did not run visualize() or if you saved and loaded the model with `save_all=False`"
69
+ )
70
+ else:
71
+ fig = self.plots["loss_absolute"]
72
+ show_figure(fig)
73
+ plt.show()
74
+ if plot_type == "relative":
75
+ if "loss_relative" not in self.plots.keys():
76
+ print("Relative loss plot not found in the plots dictionary")
77
+
78
+ print(
79
+ "This happens, when you did not run visualize() or if you saved and loaded the model with `save_all=False`"
80
+ )
81
+ else:
82
+ fig = self.plots["loss_relative"]
83
+ fig.show()
84
+ # show_figure(fig)
85
+ # plt.show()
86
+
87
+ if plot_type not in ["absolute", "relative"]:
88
+ print(
89
+ "Type of loss plot not recognized. Please use 'absolute' or 'relative'"
90
+ )
91
+
92
+ def show_evaluation(
93
+ self,
94
+ param: str,
95
+ metric: str,
96
+ ml_alg: Optional[str] = None,
97
+ ) -> None:
98
+ """
99
+ Displays the evaluation plot for a specific clinical parameter, metric, and optionally ML algorithm.
100
+ Args:
101
+ param: clinical parameter to visualize.
102
+ metric: metric to visualize.
103
+ ml_alg: ML algorithm to visualize. If None, plots all available algorithms.
104
+ Returns:
105
+ None
106
+ """
107
+ plt.ioff()
108
+ if "ML_Evaluation" not in self.plots.keys():
109
+ print("ML Evaluation plots not found in the plots dictionary")
110
+ print("You need to run evaluate() method first")
111
+ return None
112
+ if param not in self.plots["ML_Evaluation"].keys():
113
+ print(f"Parameter {param} not found in the ML Evaluation plots")
114
+ print(f"Available parameters: {list(self.plots['ML_Evaluation'].keys())}")
115
+ return None
116
+ if metric not in self.plots["ML_Evaluation"][param].keys():
117
+ print(f"Metric {metric} not found in the ML Evaluation plots for {param}")
118
+ print(
119
+ f"Available metrics: {list(self.plots['ML_Evaluation'][param].keys())}"
120
+ )
121
+ return None
122
+
123
+ algs = list(self.plots["ML_Evaluation"][param][metric].keys())
124
+ if ml_alg is not None:
125
+ if ml_alg not in algs:
126
+ print(f"ML algorithm {ml_alg} not found for {param} and {metric}")
127
+ print(f"Available ML algorithms: {algs}")
128
+ return None
129
+ fig = self.plots["ML_Evaluation"][param][metric][ml_alg].figure
130
+ show_figure(fig)
131
+ plt.show()
132
+ else:
133
+ for alg in algs:
134
+ print(f"Showing plot for ML algorithm: {alg}")
135
+ fig = self.plots["ML_Evaluation"][param][metric][alg].figure
136
+ show_figure(fig)
137
+ plt.show()
138
+
139
+ def save_plots(
140
+ self, path: str, which: Union[str, list] = "all", format: str = "png"
141
+ ) -> None:
142
+ """
143
+ Save specified plots to the given path in the specified format.
144
+
145
+ Args:
146
+ path: The directory path where the plots will be saved.
147
+ which: A list of plot names to save or a string specifying which plots to save.
148
+ If 'all', all plots in the plots dictionary will be saved.
149
+ If a single plot name is provided as a string, only that plot will be saved.
150
+ format: The file format in which to save the plots (e.g., 'png', 'jpg').
151
+
152
+ Returns:
153
+ None
154
+
155
+ Raises:
156
+ ValueError: If the 'which' parameter is not a list or a string.
157
+ """
158
+ if not os.path.exists(path):
159
+ os.makedirs(path)
160
+
161
+ if not isinstance(which, list):
162
+ ## Case when which is a string
163
+ if which == "all":
164
+ ## Case when all plots are to be saved
165
+ if len(self.plots) == 0:
166
+ print("No plots found in the plots dictionary")
167
+ print("You need to run visualize() method first")
168
+ else:
169
+ for item in nested_to_tuple(self.plots):
170
+ fig = item[-1] ## Figure is in last element of the tuple
171
+ filename = "_".join(str(x) for x in item[0:-1])
172
+ fullpath = os.path.join(path, filename)
173
+ if hasattr(fig, "savefig"):
174
+ fig.savefig(f"{fullpath}.{format}")
175
+ elif hasattr(fig, "save"): # for seaborn objects plots
176
+ fig.save(f"{fullpath}.{format}")
177
+ else:
178
+ ## Case when a single plot is provided as string
179
+ if which not in self.plots.keys():
180
+ print(f"Plot {which} not found in the plots dictionary")
181
+ print(f"All available plots are: {list(self.plots.keys())}")
182
+ else:
183
+ for item in nested_to_tuple(
184
+ self.plots[which]
185
+ ): # Plot all epochs and splits of type which
186
+ fig = item[-1] ## Figure is in last element of the tuple
187
+ filename = which + "_" + "_".join(str(x) for x in item[0:-1]) # type: ignore
188
+ fullpath = os.path.join(path, filename)
189
+ if hasattr(fig, "savefig"):
190
+ fig.savefig(f"{fullpath}.{format}")
191
+ elif hasattr(fig, "save"): # for seaborn objects plots
192
+ fig.save(f"{fullpath}.{format}")
193
+ else:
194
+ ## Case when which is a list of plot specified as strings
195
+ for key in which:
196
+ if key not in self.plots.keys():
197
+ print(f"Plot {key} not found in the plots dictionary")
198
+ print(f"All available plots are: {list(self.plots.keys())}")
199
+ continue
200
+ else:
201
+ for item in nested_to_tuple(
202
+ self.plots[key]
203
+ ): # Plot all epochs and splits of type key
204
+ fig = item[-1] ## Figure is in last element of the tuple
205
+ filename = key + "_" + "_".join(str(x) for x in item[0:-1])
206
+ fullpath = os.path.join(path, filename)
207
+ if hasattr(fig, "savefig"):
208
+ fig.savefig(f"{fullpath}.{format}")
209
+ elif hasattr(fig, "save"): # for seaborn objects plots
210
+ fig.save(f"{fullpath}.{format}")
211
+
212
+ ### Utilities ###
213
+
214
+ @staticmethod
215
+ def _make_loss_format(result: Result, config: DefaultConfig) -> pd.DataFrame:
216
+ loss_df_melt = pd.DataFrame()
217
+ for term in result.sub_losses.keys():
218
+ # Get the loss values and ensure it's a dictionary
219
+ loss_values = result.sub_losses.get(key=term).get()
220
+
221
+ # Add explicit type checking/conversion
222
+ if not isinstance(loss_values, dict):
223
+ # If it's not a dict, try to convert it or handle appropriately
224
+ if hasattr(loss_values, "to_dict"):
225
+ loss_values = loss_values.to_dict() # type: ignore
226
+ else:
227
+ # For non-convertible types, you might need a custom solution
228
+ # For numpy arrays, you could do something like:
229
+ if hasattr(loss_values, "shape"):
230
+ # For numpy arrays, create a dict with indices as keys
231
+ loss_values = {i: val for i, val in enumerate(loss_values)}
232
+
233
+ # Now create the DataFrame
234
+ loss_df = pd.DataFrame.from_dict(loss_values, orient="index") # type: ignore
235
+
236
+ # Rest of your code remains the same
237
+ if term == "var_loss":
238
+ loss_df = loss_df * config.beta
239
+ loss_df["Epoch"] = loss_df.index + 1
240
+ loss_df["Loss Term"] = term
241
+
242
+ loss_df_melt = pd.concat(
243
+ [
244
+ loss_df_melt,
245
+ loss_df.melt(
246
+ id_vars=["Epoch", "Loss Term"],
247
+ var_name="Split",
248
+ value_name="Loss Value",
249
+ ),
250
+ ],
251
+ axis=0,
252
+ ).reset_index(drop=True)
253
+
254
+ # Similar handling for the total losses
255
+ loss_values = result.losses.get()
256
+ if not isinstance(loss_values, dict):
257
+ if hasattr(loss_values, "to_dict"):
258
+ loss_values = loss_values.to_dict() # type: ignore
259
+ else:
260
+ if hasattr(loss_values, "shape"):
261
+ loss_values = {i: val for i, val in enumerate(loss_values)}
262
+
263
+ loss_df = pd.DataFrame.from_dict(loss_values, orient="index") # type: ignore
264
+ loss_df["Epoch"] = loss_df.index + 1
265
+ loss_df["Loss Term"] = "total_loss"
266
+
267
+ loss_df_melt = pd.concat(
268
+ [
269
+ loss_df_melt,
270
+ loss_df.melt(
271
+ id_vars=["Epoch", "Loss Term"],
272
+ var_name="Split",
273
+ value_name="Loss Value",
274
+ ),
275
+ ],
276
+ axis=0,
277
+ ).reset_index(drop=True)
278
+
279
+ loss_df_melt["Loss Value"] = loss_df_melt["Loss Value"].astype(float)
280
+ return loss_df_melt
281
+
282
+ @staticmethod
283
+ def _make_loss_plot(
284
+ df_plot: pd.DataFrame, plot_type: str
285
+ ) -> matplotlib.figure.Figure: # type: ignore
286
+ """
287
+ Generates a plot for visualizing loss values from a DataFrame.
288
+
289
+ Args:
290
+ df_plot : DataFrame containing the loss values to be plotted. It should have the columns:
291
+ - "Loss Term": The type of loss term (e.g., "total_loss", "reconstruction_loss").
292
+ - "Epoch": The epoch number.
293
+ - "Loss Value": The value of the loss.
294
+ - "Split": The data split (e.g., "train", "validation").
295
+
296
+ plot_type: The type of plot to generate. It can be either "absolute" or "relative".
297
+ - "absolute": Generates a line plot for each unique loss term.
298
+ - "relative": Generates a density plot for each data split, excluding the "total_loss" term.
299
+
300
+ Returns:
301
+ The generated matplotlib figure containing the loss plots.
302
+ """
303
+ fig_width_abs = 5 * len(df_plot["Loss Term"].unique())
304
+ fig_width_rel = 5 * len(df_plot["Split"].unique())
305
+ if plot_type == "absolute":
306
+ fig, axes = plt.subplots(
307
+ 1,
308
+ len(df_plot["Loss Term"].unique()),
309
+ figsize=(fig_width_abs, 5),
310
+ sharey=False,
311
+ )
312
+ ax = 0
313
+ for term in df_plot["Loss Term"].unique():
314
+ axes[ax] = sns.lineplot(
315
+ data=df_plot[(df_plot["Loss Term"] == term)],
316
+ x="Epoch",
317
+ y="Loss Value",
318
+ hue="Split",
319
+ ax=axes[ax],
320
+ ).set_title(term)
321
+ ax += 1
322
+
323
+ plt.close()
324
+
325
+ if plot_type == "relative":
326
+ # Check if loss values are positive
327
+ if (df_plot["Loss Value"] < 0).any():
328
+ # Warning
329
+ warnings.warn(
330
+ "Loss values contain negative values. Check your loss function if correct. Loss will be clipped to zero for plotting."
331
+ )
332
+ df_plot["Loss Value"] = df_plot["Loss Value"].clip(lower=0)
333
+
334
+ # Exclude loss terms where all Loss Value are zero or NaN over all epochs
335
+ valid_terms = [
336
+ term
337
+ for term in df_plot["Loss Term"].unique()
338
+ if (
339
+ (df_plot[df_plot["Loss Term"] == term]["Loss Value"].notna().any())
340
+ and (df_plot[df_plot["Loss Term"] == term]["Loss Value"] != 0).any()
341
+ )
342
+ ]
343
+ exclude = (
344
+ (df_plot["Loss Term"] != "total_loss")
345
+ & ~(df_plot["Loss Term"].str.contains("_factor"))
346
+ & (df_plot["Loss Term"].isin(valid_terms))
347
+ )
348
+
349
+ df_plot.loc[exclude, "Relative Loss Value"] = (
350
+ df_plot[exclude]
351
+ .groupby(["Split", "Epoch"])["Loss Value"]
352
+ .transform(lambda x: x / x.sum())
353
+ )
354
+ fig = (
355
+ (
356
+ so.Plot(
357
+ df_plot[exclude],
358
+ "Epoch",
359
+ "Relative Loss Value",
360
+ color="Loss Term",
361
+ ).add(so.Area(alpha=0.7), so.Stack())
362
+ )
363
+ .facet("Split")
364
+ .layout(size=(fig_width_rel, 5))
365
+ )
366
+
367
+ # fig, axes = plt.subplots(1, 2, figsize=(fig_width_rel, 5), sharey=True)
368
+
369
+ # ax = 0
370
+
371
+ # for split in df_plot["Split"].unique():
372
+ # axes[ax] = sns.kdeplot(
373
+ # data=df_plot[exclude & (df_plot["Split"] == split)],
374
+ # x="Epoch",
375
+ # hue="Loss Term",
376
+ # multiple="fill",
377
+ # weights="Loss Value",
378
+ # clip=[0, df_plot["Epoch"].max()],
379
+ # ax=axes[ax],
380
+ # ).set_title(split)
381
+ # ax += 1
382
+
383
+ # plt.close()
384
+
385
+ return fig
386
+
387
+ @staticmethod
388
+ def _plot_model_weights(model: torch.nn.Module) -> matplotlib.figure.Figure: # type: ignore
389
+ """
390
+ Visualization of model weights in encoder and decoder layers as heatmap for each layer as subplot.
391
+ Handles non-symmetrical autoencoder architectures.
392
+ Plots _mu layer for encoder as well.
393
+ Uses node_names for decoder layers if model has ontologies.
394
+ ARGS:
395
+ model (torch.nn.Module): PyTorch model instance.
396
+ RETURNS:
397
+ fig (matplotlib.figure): Figure handle (of last plot)
398
+ """
399
+ all_weights = []
400
+ names = []
401
+ node_names = None
402
+ if hasattr(model, "ontologies"):
403
+ if model.ontologies is not None:
404
+ node_names = []
405
+ for ontology in model.ontologies:
406
+ node_names.append(list(ontology.keys()))
407
+ node_names.append(model.feature_order)
408
+
409
+ # Collect encoder and decoder weights separately
410
+ encoder_weights = []
411
+ encoder_names = []
412
+ decoder_weights = []
413
+ decoder_names = []
414
+ for name, param in model.named_parameters():
415
+ # print(name)
416
+ if "weight" in name and len(param.shape) == 2:
417
+ if "encoder" in name and "var" not in name and "_mu" not in name:
418
+ encoder_weights.append(param.detach().cpu().numpy())
419
+ encoder_names.append(name[:-7])
420
+ elif "_mu" in name:
421
+ encoder_weights.append(param.detach().cpu().numpy())
422
+ encoder_names.append(name[:-7])
423
+ elif "decoder" in name and "var" not in name:
424
+ decoder_weights.append(param.detach().cpu().numpy())
425
+ decoder_names.append(name[:-7])
426
+ elif (
427
+ "encoder" not in name
428
+ and "decoder" not in name
429
+ and "var" not in name
430
+ ):
431
+ # fallback for models without explicit encoder/decoder in name
432
+ all_weights.append(param.detach().cpu().numpy())
433
+ names.append(name[:-7])
434
+
435
+ if encoder_weights or decoder_weights:
436
+ n_enc = len(encoder_weights)
437
+ n_dec = len(decoder_weights)
438
+ n_cols = max(n_enc, n_dec)
439
+ fig, axes = plt.subplots(2, n_cols, sharex=False, figsize=(15 * n_cols, 15))
440
+ if n_cols == 1:
441
+ axes = axes.reshape(2, 1)
442
+ # Plot encoder weights
443
+ for i in range(n_enc):
444
+ ax = axes[0, i]
445
+ sns.heatmap(
446
+ encoder_weights[i],
447
+ cmap=sns.color_palette("Spectral", as_cmap=True),
448
+ center=0,
449
+ ax=ax,
450
+ ).set(title=encoder_names[i])
451
+ ax.set_ylabel("Out Node", size=12)
452
+ # Hide unused encoder subplots
453
+ for i in range(n_enc, n_cols):
454
+ axes[0, i].axis("off")
455
+ # Plot decoder weights
456
+ for i in range(n_dec):
457
+ ax = axes[1, i]
458
+ heatmap_kwargs = {}
459
+
460
+ sns.heatmap(
461
+ decoder_weights[i],
462
+ cmap=sns.color_palette("Spectral", as_cmap=True),
463
+ center=0,
464
+ ax=ax,
465
+ **heatmap_kwargs,
466
+ ).set(title=decoder_names[i])
467
+ if model.ontologies is not None:
468
+ axes[1, i].set_xticks(
469
+ ticks=range(len(node_names[i])), # type: ignore
470
+ labels=node_names[i], # type: ignore
471
+ rotation=90,
472
+ fontsize=8,
473
+ )
474
+ axes[1, i].set_yticks(
475
+ ticks=range(len(node_names[i + 1])), # type: ignore
476
+ labels=node_names[i + 1], # type: ignore
477
+ rotation=0,
478
+ fontsize=8,
479
+ )
480
+ ax.set_xlabel("In Node", size=12)
481
+ ax.set_ylabel("Out Node", size=12)
482
+ # Hide unused decoder subplots
483
+ for i in range(n_dec, n_cols):
484
+ axes[1, i].axis("off")
485
+ else:
486
+ # fallback: plot all weights in order, split in half for encoder/decoder
487
+ n_layers = len(all_weights) // 2
488
+ fig, axes = plt.subplots(
489
+ 2, n_layers, sharex=False, figsize=(5 * n_layers, 10)
490
+ )
491
+ for layer in range(n_layers):
492
+ sns.heatmap(
493
+ all_weights[layer],
494
+ cmap=sns.color_palette("Spectral", as_cmap=True),
495
+ center=0,
496
+ ax=axes[0, layer],
497
+ ).set(title=names[layer])
498
+ sns.heatmap(
499
+ all_weights[n_layers + layer],
500
+ cmap=sns.color_palette("Spectral", as_cmap=True),
501
+ center=0,
502
+ ax=axes[1, layer],
503
+ ).set(title=names[n_layers + layer])
504
+ axes[1, layer].set_xlabel("In Node", size=12)
505
+ axes[0, layer].set_ylabel("Out Node", size=12)
506
+ axes[1, layer].set_ylabel("Out Node", size=12)
507
+
508
+ fig.suptitle("Model Weights", size=20)
509
+ plt.close()
510
+ return fig
511
+
512
+ @staticmethod
513
+ def _collect_all_metadata(result):
514
+ all_metadata = pd.DataFrame()
515
+
516
+ # 1) collect metadata from results.datasets
517
+
518
+ # 1a) iterate over splits [train, valid, test] if they exist
519
+ for split in ["train", "valid", "test"]:
520
+
521
+ if hasattr(result.datasets, split) and result.datasets[split] is not None:
522
+ if hasattr(result.datasets[split], "metadata"):
523
+ split_metadata = result.datasets[split].metadata
524
+
525
+ # 1b) if result.datasets.split is a dictionary, iterate over keys (modalities)
526
+ if isinstance(split_metadata, dict):
527
+ for modality, modality_data in split_metadata.items():
528
+ all_metadata = pd.concat(
529
+ [all_metadata, modality_data], axis=0
530
+ )
531
+ # 1c) if result.datasets.split is a Dataframe, just collect metadata directly
532
+ elif isinstance(split_metadata, pd.DataFrame):
533
+ all_metadata = pd.concat([all_metadata, split_metadata], axis=0)
534
+ else:
535
+ split_modalities = result.datasets[split].datasets
536
+ if isinstance(split_modalities, dict):
537
+ for modality, modality_data in split_modalities.items():
538
+ if hasattr(modality_data, "metadata"):
539
+ modality_metadata = modality_data.metadata
540
+ if isinstance(modality_metadata, pd.DataFrame):
541
+ all_metadata = pd.concat(
542
+ [all_metadata, modality_metadata], axis=0
543
+ )
544
+
545
+ # 2) collect metadata from results.new_datasets in the same way
546
+ if hasattr(result, "new_datasets"):
547
+ for split in ["train", "valid", "test"]:
548
+ if (
549
+ hasattr(result.new_datasets, split)
550
+ and result.new_datasets[split] is not None
551
+ ):
552
+ if hasattr(result.new_datasets[split], "metadata"):
553
+ split_metadata = result.new_datasets[split].metadata
554
+
555
+ if isinstance(split_metadata, dict):
556
+ for modality, modality_data in split_metadata.items():
557
+ all_metadata = pd.concat(
558
+ [all_metadata, modality_data], axis=0
559
+ )
560
+ elif isinstance(split_metadata, pd.DataFrame):
561
+ all_metadata = pd.concat(
562
+ [all_metadata, split_metadata], axis=0
563
+ )
564
+ else:
565
+ split_modalities = result.new_datasets[split].datasets
566
+ if isinstance(split_modalities, dict):
567
+ for modality, modality_data in split_modalities.items():
568
+ if hasattr(modality_data, "metadata"):
569
+ modality_metadata = modality_data.metadata
570
+ if isinstance(modality_metadata, pd.DataFrame):
571
+ all_metadata = pd.concat(
572
+ [all_metadata, modality_metadata], axis=0
573
+ )
574
+
575
+ # Remove duplicate rows if any
576
+ all_metadata = all_metadata.loc[~all_metadata.index.duplicated(keep="first")]
577
+
578
+ return all_metadata