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.
- structboost/__init__.py +94 -0
- structboost/_annotation.py +266 -0
- structboost/_boosting.py +552 -0
- structboost/_decoder.py +109 -0
- structboost/_encoder.py +88 -0
- structboost/_explorer.py +842 -0
- structboost/_io.py +302 -0
- structboost/_model.py +3483 -0
- structboost/_persistence.py +326 -0
- structboost/_plotting.py +301 -0
- structboost/_simulation.py +867 -0
- structboost/_stability.py +412 -0
- structboost/_types.py +393 -0
- structboost/_utils.py +509 -0
- structboost/py.typed +0 -0
- structboost-0.1.0.dist-info/METADATA +219 -0
- structboost-0.1.0.dist-info/RECORD +19 -0
- structboost-0.1.0.dist-info/WHEEL +4 -0
- structboost-0.1.0.dist-info/licenses/LICENSE +21 -0
structboost/__init__.py
ADDED
|
@@ -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)
|