structboost 0.1.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.
@@ -0,0 +1,94 @@
1
+ """structboost - A scverse-ecosystem package for Boosting Autoencoders.
2
+
3
+ This module keeps imports lightweight by lazily importing optional / heavy
4
+ dependencies (notably `torch`) only when needed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib.metadata import PackageNotFoundError, version
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from ._annotation import (
13
+ DimensionAnnotation,
14
+ DimensionGeneRanking,
15
+ GeneRanking,
16
+ extract_gene_rankings,
17
+ write_annotations_to_h5ad,
18
+ )
19
+ from ._boosting import AllboostHistory, allboost
20
+ from ._io import looks_like_ensembl, read_encoder_weights, write_encoder_weights
21
+ from ._simulation import SimulationResult, sim_scrnaseq_anndata, sim_scrnaseq_data
22
+ from ._stability import StabilitySelectionResult, stability_selection
23
+ from ._types import BAEConfig, TrainingReport
24
+ from ._utils import (
25
+ ObsCovariateEncoding,
26
+ compute_covariance_cache,
27
+ encode_obs_covariates,
28
+ linear_ceiling,
29
+ transform_obs_covariates,
30
+ )
31
+
32
+ if TYPE_CHECKING: # pragma: no cover
33
+ # For type checkers only (avoids importing torch at runtime)
34
+ from ._model import BAE as BAE
35
+
36
+ try:
37
+ __version__ = version("structboost")
38
+ except PackageNotFoundError: # pragma: no cover
39
+ # Fallback for editable checkouts where metadata is not available.
40
+ __version__ = "0.1.0"
41
+
42
+ __all__ = [
43
+ "AllboostHistory",
44
+ "BAE",
45
+ "BAEConfig",
46
+ "DimensionAnnotation",
47
+ "DimensionGeneRanking",
48
+ "GeneRanking",
49
+ "ObsCovariateEncoding",
50
+ "SimulationResult",
51
+ "StabilitySelectionResult",
52
+ "TrainingReport",
53
+ "allboost",
54
+ "compute_covariance_cache",
55
+ "encode_obs_covariates",
56
+ "export_interactive_html",
57
+ "extract_gene_rankings",
58
+ "linear_ceiling",
59
+ "looks_like_ensembl",
60
+ "plot_boosting_coefficient_paths",
61
+ "plot_top_boosting_coefficients",
62
+ "plot_training_diagnostics",
63
+ "read_encoder_weights",
64
+ "sim_scrnaseq_anndata",
65
+ "sim_scrnaseq_data",
66
+ "stability_selection",
67
+ "transform_obs_covariates",
68
+ "write_annotations_to_h5ad",
69
+ "write_encoder_weights",
70
+ ]
71
+
72
+ _PLOT_FUNCTIONS = frozenset(
73
+ {
74
+ "plot_boosting_coefficient_paths",
75
+ "plot_top_boosting_coefficients",
76
+ "plot_training_diagnostics",
77
+ }
78
+ )
79
+
80
+
81
+ def __getattr__(name: str) -> Any: # PEP 562
82
+ if name == "BAE":
83
+ from ._model import BAE as _BAE
84
+
85
+ return _BAE
86
+ if name == "export_interactive_html":
87
+ from ._explorer import export_interactive_html as _export_interactive_html
88
+
89
+ return _export_interactive_html
90
+ if name in _PLOT_FUNCTIONS:
91
+ from . import _plotting
92
+
93
+ return getattr(_plotting, name)
94
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,266 @@
1
+ """Annotation helpers for BAE latent dimension interpretation.
2
+
3
+ Provides extraction of ranked gene lists per latent dimension from fitted
4
+ AnnData objects and writing functional annotations back to h5ad files.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass
14
+ class GeneRanking:
15
+ """Ranked gene list with weights.
16
+
17
+ Attributes
18
+ ----------
19
+ gene_names
20
+ Gene names ordered by descending magnitude.
21
+ weights
22
+ Corresponding encoder weights (same order as gene_names).
23
+ """
24
+
25
+ gene_names: list[str]
26
+ weights: list[float]
27
+
28
+
29
+ @dataclass
30
+ class DimensionGeneRanking:
31
+ """Ranked gene lists (positive and negative) for a single latent dimension.
32
+
33
+ Attributes
34
+ ----------
35
+ dimension
36
+ Index of the latent dimension.
37
+ n_selected_genes
38
+ Total number of nonzero genes in this dimension.
39
+ positive_genes
40
+ Top-k positively weighted genes, ranked by weight descending.
41
+ negative_genes
42
+ Top-k negatively weighted genes, ranked by absolute weight descending.
43
+ """
44
+
45
+ dimension: int
46
+ n_selected_genes: int
47
+ positive_genes: GeneRanking
48
+ negative_genes: GeneRanking
49
+
50
+
51
+ @dataclass
52
+ class DimensionAnnotation:
53
+ """Functional annotation for a single latent dimension.
54
+
55
+ Attributes
56
+ ----------
57
+ dimension
58
+ Index of the latent dimension.
59
+ positive_annotation
60
+ Functional description of the positive gene program.
61
+ negative_annotation
62
+ Functional description of the negative gene program.
63
+ overall_annotation
64
+ Overall biological axis / cell type assignment for this dimension.
65
+ database_references
66
+ Relevant database URLs supporting the annotation.
67
+ """
68
+
69
+ dimension: int
70
+ positive_annotation: str
71
+ negative_annotation: str
72
+ overall_annotation: str
73
+ database_references: list[str] = field(default_factory=list)
74
+
75
+
76
+ def extract_gene_rankings(
77
+ adata_path: str | Path,
78
+ model_key: str = "BAE_encoder_weights",
79
+ dimensions: list[int] | None = None,
80
+ top_k: int = 50,
81
+ ) -> list[DimensionGeneRanking]:
82
+ """Extract ranked gene lists (pos/neg) per latent dimension from .h5ad.
83
+
84
+ For each dimension, genes are split by sign of their encoder weight.
85
+ Within each group, genes are ranked by magnitude (descending).
86
+ At most ``min(top_k, n_available)`` genes are returned per group.
87
+
88
+ Parameters
89
+ ----------
90
+ adata_path
91
+ Path to a .h5ad file containing fitted BAE results.
92
+ model_key
93
+ Key in ``adata.varm`` holding the encoder weight matrix,
94
+ e.g. ``"BAE_encoder_weights"``.
95
+ dimensions
96
+ Indices of latent dimensions to extract. ``None`` extracts all.
97
+ top_k
98
+ Maximum number of genes per sign group (positive/negative).
99
+ Clamped to ``min(top_k, n_available)`` when fewer genes exist.
100
+
101
+ Returns
102
+ -------
103
+ list[DimensionGeneRanking]
104
+ Ranked gene lists for each requested dimension.
105
+
106
+ Raises
107
+ ------
108
+ FileNotFoundError
109
+ If ``adata_path`` does not exist.
110
+ KeyError
111
+ If ``model_key`` is not found in ``adata.varm``.
112
+ """
113
+ import anndata as ad
114
+ import numpy as np
115
+
116
+ adata_path = Path(adata_path)
117
+ if not adata_path.exists():
118
+ raise FileNotFoundError(f"File not found: {adata_path}")
119
+
120
+ adata = ad.read_h5ad(adata_path)
121
+
122
+ if model_key not in adata.varm:
123
+ raise KeyError(
124
+ f"{model_key!r} not found in adata.varm. Available keys: {list(adata.varm.keys())}"
125
+ )
126
+
127
+ W = np.asarray(adata.varm[model_key]) # (n_genes, latent_dim)
128
+ gene_names = list(adata.var_names)
129
+ _n_genes, latent_dim = W.shape
130
+
131
+ if dimensions is None:
132
+ dimensions = list(range(latent_dim))
133
+
134
+ rankings: list[DimensionGeneRanking] = []
135
+ for dim in dimensions:
136
+ w = W[:, dim]
137
+
138
+ # Positive genes: w > 0, sorted by weight descending
139
+ pos_mask = w > 0
140
+ pos_indices = np.where(pos_mask)[0]
141
+ pos_order = np.argsort(-w[pos_indices])
142
+ pos_indices = pos_indices[pos_order][:top_k]
143
+ pos_names = [gene_names[i] for i in pos_indices]
144
+ pos_weights = [float(w[i]) for i in pos_indices]
145
+
146
+ # Negative genes: w < 0, sorted by |weight| descending
147
+ neg_mask = w < 0
148
+ neg_indices = np.where(neg_mask)[0]
149
+ neg_order = np.argsort(w[neg_indices]) # most negative first
150
+ neg_indices = neg_indices[neg_order][:top_k]
151
+ neg_names = [gene_names[i] for i in neg_indices]
152
+ neg_weights = [float(w[i]) for i in neg_indices]
153
+
154
+ n_selected = int(pos_mask.sum() + neg_mask.sum())
155
+
156
+ rankings.append(
157
+ DimensionGeneRanking(
158
+ dimension=dim,
159
+ n_selected_genes=n_selected,
160
+ positive_genes=GeneRanking(gene_names=pos_names, weights=pos_weights),
161
+ negative_genes=GeneRanking(gene_names=neg_names, weights=neg_weights),
162
+ )
163
+ )
164
+
165
+ return rankings
166
+
167
+
168
+ _KNOWN_MODEL_KEYS = ("BAE_encoder_weights",)
169
+
170
+
171
+ def detect_model_keys(adata_path: str | Path) -> list[str]:
172
+ """Detect which BAE encoder weight keys are present in a .h5ad file.
173
+
174
+ Parameters
175
+ ----------
176
+ adata_path
177
+ Path to a .h5ad file.
178
+
179
+ Returns
180
+ -------
181
+ list[str]
182
+ Model keys found in ``adata.varm`` (subset of
183
+ ``["BAE_encoder_weights"]``).
184
+ """
185
+ import anndata as ad
186
+
187
+ adata = ad.read_h5ad(Path(adata_path))
188
+ return [k for k in _KNOWN_MODEL_KEYS if k in adata.varm]
189
+
190
+
191
+ def extract_metadata(adata_path: str | Path) -> dict[str, str | None]:
192
+ """Extract biological metadata from adata.uns if present.
193
+
194
+ Looks for common keys: ``organism``, ``tissue``, ``species``, ``organ``,
195
+ ``experiment``. Falls back to ``"species"`` for ``"organism"`` and
196
+ ``"organ"`` for ``"tissue"`` if the primary keys are absent.
197
+
198
+ Parameters
199
+ ----------
200
+ adata_path
201
+ Path to a .h5ad file.
202
+
203
+ Returns
204
+ -------
205
+ dict[str, str | None]
206
+ Keys: ``"organism"``, ``"tissue"``, ``"experiment"``. Values are
207
+ ``None`` if not found.
208
+ """
209
+ import anndata as ad
210
+
211
+ adata = ad.read_h5ad(Path(adata_path))
212
+ uns = adata.uns
213
+
214
+ organism = uns.get("organism") or uns.get("species")
215
+ tissue = uns.get("tissue") or uns.get("organ")
216
+ experiment = uns.get("experiment")
217
+
218
+ return {
219
+ "organism": str(organism) if organism is not None else None,
220
+ "tissue": str(tissue) if tissue is not None else None,
221
+ "experiment": str(experiment) if experiment is not None else None,
222
+ }
223
+
224
+
225
+ def write_annotations_to_h5ad(
226
+ adata_path: str | Path,
227
+ annotations: list[DimensionAnnotation],
228
+ ) -> None:
229
+ """Write functional annotations into adata.uns of a .h5ad file.
230
+
231
+ Annotations are stored under ``adata.uns["bae_dimension_annotations"]``
232
+ as a dict keyed by dimension index (string).
233
+
234
+ .. warning::
235
+ The file is read, mutated and written back over the same path. Keep a copy
236
+ if the original matters.
237
+
238
+ See Also
239
+ --------
240
+ extract_gene_rankings : Produce the ranked gene lists to annotate.
241
+
242
+ Parameters
243
+ ----------
244
+ adata_path
245
+ Path to a .h5ad file.
246
+ annotations
247
+ List of :class:`DimensionAnnotation` objects to write.
248
+ """
249
+ import anndata as ad
250
+
251
+ adata_path = Path(adata_path)
252
+ adata = ad.read_h5ad(adata_path)
253
+
254
+ uns_key = "bae_dimension_annotations"
255
+
256
+ annotation_dict: dict[str, dict[str, str | list[str]]] = {}
257
+ for ann in annotations:
258
+ annotation_dict[str(ann.dimension)] = {
259
+ "positive_annotation": ann.positive_annotation,
260
+ "negative_annotation": ann.negative_annotation,
261
+ "overall_annotation": ann.overall_annotation,
262
+ "database_references": ann.database_references,
263
+ }
264
+
265
+ adata.uns[uns_key] = annotation_dict
266
+ adata.write_h5ad(adata_path)