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,318 @@
1
+ import os
2
+ import re
3
+ import json
4
+ import ollama
5
+ from dotenv import find_dotenv, load_dotenv
6
+ from mistralai.client import Mistral
7
+ from typing import List, Dict, Any
8
+
9
+ import requests
10
+ import warnings
11
+
12
+ warnings.simplefilter("always", UserWarning)
13
+
14
+
15
+ class LLMExplainer:
16
+ """LLM client with support for multiple providers."""
17
+
18
+ def __init__(
19
+ self,
20
+ client_name: str,
21
+ model_name: str,
22
+ genes_to_latent: Dict[str, List],
23
+ prompt: str,
24
+ ):
25
+ """Initialize LLM client.
26
+
27
+ Args:
28
+ client_name: Name of the LLM client.
29
+ model_name: Name of the model to use.
30
+ """
31
+ load_dotenv(find_dotenv())
32
+ self._client_name = client_name
33
+ self._model = model_name
34
+ self.prompt = prompt
35
+ self.genes_to_latent = genes_to_latent
36
+ if self._client_name == "ollama":
37
+ # Set Ollama host for Docker compatibility
38
+ ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434")
39
+ # Configure ollama client
40
+ ollama_client = ollama.Client(host=ollama_host)
41
+ self._ollama_client = ollama_client
42
+ try:
43
+ response = self._ollama_client.list()
44
+ available_models = [m.model for m in response.models]
45
+ if self._model not in available_models:
46
+ raise ValueError(
47
+ f"Model '{self._model}' not available. "
48
+ f"Available: {available_models}"
49
+ )
50
+ except Exception as e:
51
+ import warnings
52
+
53
+ warnings.warn(f"Could not validate Ollama model '{self._model}': {e}")
54
+ # Initialize client-specific objects
55
+ elif self._client_name == "mistral":
56
+ api_key = os.environ.get("MISTRAL_API_KEY")
57
+ if not api_key:
58
+ raise ValueError("Environment variable MISTRAL_API_KEY not set")
59
+ self._mistral_client = Mistral(api_key=api_key)
60
+ elif self._client_name == "openrouter":
61
+ self._openrouter_api_key = os.environ.get("OPENROUTER_PREMIUM_API_KEY")
62
+ if not self._openrouter_api_key:
63
+ raise ValueError(
64
+ "Environment variable OPENROUTER_PREMIUM_API_KEY not set"
65
+ )
66
+ self._openrouter_url = "https://openrouter.ai/api/v1/chat/completions"
67
+ elif self._client_name == "scads-llm":
68
+ self._scads_llm_api_key = os.environ.get("SCADS_LLM_API_KEY")
69
+ if not self._scads_llm_api_key:
70
+ raise ValueError("Environment variable SCADS_LLM_API_KEY not set")
71
+ self._scads_llm_url = "https://llm.scads.ai/v1"
72
+ else:
73
+ raise NotImplementedError(f"Client {self._client_name} not implemented")
74
+
75
+ def _build_prompt(self, *, gene_list: List[str], prompt: str) -> str:
76
+ """Builds the prompt for the LLM.
77
+
78
+ Args:
79
+ gene_list: List of genes.
80
+ prompt: The prompt template.
81
+
82
+ Returns:
83
+ The formatted prompt.
84
+ """
85
+ gene_block = "\n".join(f"- {g}" for g in gene_list)
86
+ return prompt.format(gene_block=gene_block)
87
+
88
+ def extract_json_from_output(self, text: str) -> Dict[str, Any]:
89
+ """Extract and parse JSON from LLM output robustly.
90
+
91
+ Args:
92
+ text: LLM output string.
93
+
94
+ Returns:
95
+ Parsed dict with keys "TLDR" and "DETAILS".
96
+ """
97
+ # Strip whitespace and common wrappers
98
+ text = text.strip()
99
+ # Remove code block markers if present
100
+ text = re.sub(r"^```json\s*|\s*```$", "", text, flags=re.DOTALL)
101
+ # Extract the JSON substring if embedded in text
102
+ match = re.search(r"\{.*\}", text, re.DOTALL)
103
+ if match:
104
+ text = match.group(0)
105
+ # Fix common issues: single quotes to double, trailing commas
106
+ text = text.replace("'", '"')
107
+ text = re.sub(r",\s*([}\]])", r"\1", text)
108
+ # Remove unescaped newlines in strings (approximate fix)
109
+ text = re.sub(r"(?<!\\)\n", " ", text)
110
+ try:
111
+ return json.loads(text)
112
+ except json.JSONDecodeError as e:
113
+ import warnings
114
+
115
+ warnings.warn(f"Failed to parse JSON. Returning raw text. Error: {e}")
116
+ return {
117
+ "TLDR": text,
118
+ "DETAILS": {
119
+ "dominant_themes": text,
120
+ "hypotheses": [],
121
+ "pathways_summary": text,
122
+ },
123
+ }
124
+
125
+ def explain(self) -> Dict[str, Dict[str, Any]]:
126
+ """Generate explanations for each latent dimension.
127
+
128
+ Returns:
129
+ A dict mapping latent dimension -> parsed JSON explanation (or raw output if parsing fails).
130
+ """
131
+ res: Dict[str, Dict[str, Any]] = {}
132
+ markdown_sections = []
133
+ for key, genes in self.genes_to_latent.items():
134
+ print(f"Explaining latent dimension {key} with LLM...")
135
+ prompt = self._build_prompt(gene_list=genes, prompt=self.prompt)
136
+ raw_output = self._get_llm_answer(question=prompt)
137
+ if len(raw_output) == 0:
138
+ import warnings
139
+
140
+ warnings.warn(
141
+ f"Received empty response from LLM for latent dimension {key}. Skipping."
142
+ )
143
+ continue
144
+ # ---- TRY TO PARSE JSON ----
145
+ try:
146
+ parsed = self.extract_json_from_output(raw_output)
147
+ except Exception as e:
148
+ import warnings
149
+
150
+ warnings.warn(
151
+ f"Failed to parse JSON for latent dimension {key}. "
152
+ f"Using raw output instead. Error: {e}"
153
+ )
154
+ parsed = {
155
+ "TLDR": raw_output,
156
+ "DETAILS": {
157
+ "dominant_themes": raw_output,
158
+ "hypotheses": [],
159
+ "pathways_summary": raw_output,
160
+ },
161
+ }
162
+ res[key] = parsed
163
+ # ---- BUILD READABLE MARKDOWN ----
164
+ markdown_sections.append(f"# Latent Dimension {key}\n")
165
+ markdown_sections.append("## Most influential genes")
166
+ markdown_sections.append(", ".join(genes) + "\n")
167
+ markdown_sections.append("## TLDR")
168
+ markdown_sections.append(parsed.get("TLDR", "") + "\n")
169
+ markdown_sections.append("## Details\n")
170
+
171
+ details = parsed.get("DETAILS", {})
172
+ if isinstance(details, dict):
173
+ # Dominant themes
174
+ markdown_sections.append("### Dominant Biological Themes\n")
175
+ markdown_sections.append(details.get("dominant_themes", "") + "\n")
176
+ # Hypotheses
177
+ markdown_sections.append("### Mechanistic Hypotheses\n")
178
+ hyps = details.get("hypotheses", [])
179
+ if isinstance(hyps, list):
180
+ for hyp in hyps:
181
+ markdown_sections.append(f"- {hyp}\n")
182
+ else:
183
+ markdown_sections.append(str(hyps) + "\n")
184
+ # Pathways summary
185
+ markdown_sections.append("### Summary of Pathways/Processes\n")
186
+ markdown_sections.append(details.get("pathways_summary", "") + "\n")
187
+ else:
188
+ # Fallback if not dict
189
+ markdown_sections.append(str(details) + "\n")
190
+
191
+ markdown_sections.append("\n---\n")
192
+ # ---- WRITE ONE SINGLE FILE ----
193
+ output_path = os.path.join(os.getcwd(), "latent_explanations.md")
194
+ with open(output_path, "w") as f:
195
+ f.write("\n".join(markdown_sections))
196
+ print(f"Saved explanations to: {output_path}")
197
+ return res
198
+
199
+ def _get_llm_answer(self, *, question: str) -> str:
200
+ """Gets the LLM answer based on the client.
201
+
202
+ Args:
203
+ question: The input question.
204
+
205
+ Returns:
206
+ Generated response text.
207
+ """
208
+ if self._client_name == "mistral":
209
+ return self._get_mistral_answer(question=question)
210
+ elif self._client_name == "ollama":
211
+ return self._get_ollama_answer(question=question)
212
+ elif self._client_name == "openrouter":
213
+ return self._get_openrouter_answer(
214
+ question=question, model_name=self._model
215
+ )
216
+ elif self._client_name == "scads-llm":
217
+ return self._get_scads_llm_answer(question=question, model_name=self._model)
218
+ else:
219
+ raise NotImplementedError(f"Client {self._client_name} not implemented")
220
+
221
+ def _get_mistral_answer(self, *, question: str) -> str:
222
+ """Get answer from Mistral API.
223
+
224
+ Args:
225
+ question: The input question.
226
+
227
+ Returns:
228
+ Generated response text.
229
+ """
230
+ chat_response = self._mistral_client.chat.complete(
231
+ model=self._model,
232
+ messages=[
233
+ {
234
+ "role": "user",
235
+ "content": question, # type: ignore
236
+ },
237
+ ],
238
+ response_format={"type": "json_object"}, # Enforce JSON output
239
+ )
240
+ return chat_response.choices[0].message.content # type: ignore
241
+
242
+ def _get_ollama_answer(self, *, question: str) -> str:
243
+ """Get answer from Ollama.
244
+
245
+ Args:
246
+ question: The input question.
247
+
248
+ Returns:
249
+ Generated response text.
250
+ """
251
+ response = self._ollama_client.generate(
252
+ model=self._model,
253
+ prompt=question,
254
+ )
255
+ return response["response"]
256
+
257
+ def _get_openrouter_answer(self, *, question: str, model_name: str) -> str:
258
+ """Get answer from OpenRouter.
259
+
260
+ Args:
261
+ question: The input question.
262
+ model_name: The name of the model to use.
263
+
264
+ Returns:
265
+ Generated response text.
266
+ """
267
+
268
+ response = requests.post(
269
+ url=self._openrouter_url,
270
+ headers={
271
+ "Authorization": f"Bearer {self._openrouter_api_key}",
272
+ },
273
+ data=json.dumps(
274
+ {
275
+ "model": model_name,
276
+ "messages": [
277
+ {
278
+ "role": "user",
279
+ "content": question,
280
+ }
281
+ ],
282
+ }
283
+ ),
284
+ )
285
+ if response.status_code != 200:
286
+ # Warn
287
+ warnings.warn(
288
+ f"OpenRouter API request failed with status code {response.status_code}: {response.text}"
289
+ )
290
+ # Return empty response
291
+ return ""
292
+ if "choices" not in response.json() or len(response.json()["choices"]) == 0:
293
+ warnings.warn(
294
+ f"OpenRouter API response missing 'choices': {response.json()}"
295
+ )
296
+ return ""
297
+ return response.json()["choices"][0]["message"]["content"]
298
+
299
+ def _get_scads_llm_answer(self, *, question: str, model_name: str) -> str:
300
+ """Get answer from SCADS-LLM.
301
+
302
+ Args:
303
+ question: The input question.
304
+ model_name: The name of the model to use.
305
+
306
+ Returns:
307
+ Generated response text.
308
+ """
309
+
310
+ client = OpenAI(
311
+ base_url="https://llm.scads.ai/v1", api_key=self._scads_llm_api_key
312
+ )
313
+
314
+ response = client.chat.completions.create(
315
+ messages=[{"role": "user", "content": question}], model=model_name
316
+ )
317
+
318
+ return response.choices[0].message.content
@@ -0,0 +1,20 @@
1
+ """Backward-compatible re-exports of loss classes.
2
+
3
+ Each loss implementation has been moved to its own module under
4
+ ``autoencodix.utils``. This module keeps the historical import path
5
+ ``autoencodix.utils._losses`` working during the refactor.
6
+ """
7
+
8
+ from autoencodix.losses.vanillix_loss import VanillixLoss
9
+ from autoencodix.losses.varix_loss import VarixLoss
10
+ from autoencodix.losses.xmodal_loss import XModalLoss
11
+ from autoencodix.losses.disentanglix_loss import DisentanglixLoss
12
+ from autoencodix.losses.maskix_loss import MaskixLoss
13
+
14
+ __all__ = [
15
+ "VanillixLoss",
16
+ "VarixLoss",
17
+ "XModalLoss",
18
+ "DisentanglixLoss",
19
+ "MaskixLoss",
20
+ ]
@@ -0,0 +1,30 @@
1
+ from typing import Optional
2
+ from dataclasses import dataclass
3
+ import torch
4
+
5
+
6
+ # internal check done
7
+ # write tests: done
8
+ @dataclass
9
+ class ModelOutput:
10
+ """A structured output dataclass for autoencoder models.
11
+
12
+ This class is used to encapsulate the outputs of autoencoder models in a
13
+ consistent format, allowing for flexibility in the type of outputs returned
14
+ by different architectures.
15
+
16
+ Attributes:
17
+ reconstruction: The reconstructed input data.
18
+ latent_mean: The mean of the latent space distribution, applicable for models like VAEs.
19
+ latent_logvar: The log variance of the latent space distribution, applicable for models like VAEs.
20
+ additional_info: A dictionary to store any additional information or intermediate outputs.
21
+ """
22
+
23
+ reconstruction: torch.Tensor
24
+ latentspace: torch.Tensor
25
+ latent_mean: Optional[torch.Tensor] = None
26
+ latent_logvar: Optional[torch.Tensor] = None
27
+ additional_info: Optional[dict] = None
28
+
29
+ def __iter__(self):
30
+ yield self