diffbio 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.
- diffbio/__init__.py +39 -0
- diffbio/configs.py +75 -0
- diffbio/constants.py +204 -0
- diffbio/core/__init__.py +127 -0
- diffbio/core/base_operators.py +612 -0
- diffbio/core/data_types.py +260 -0
- diffbio/core/gnn_components.py +629 -0
- diffbio/core/graph_utils.py +149 -0
- diffbio/core/neural_components.py +270 -0
- diffbio/core/optimal_transport.py +133 -0
- diffbio/core/soft_ops/__init__.py +216 -0
- diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
- diffbio/core/soft_ops/_projections_simplex.py +240 -0
- diffbio/core/soft_ops/_projections_transport.py +508 -0
- diffbio/core/soft_ops/_sorting_network.py +204 -0
- diffbio/core/soft_ops/_types.py +15 -0
- diffbio/core/soft_ops/_utils.py +342 -0
- diffbio/core/soft_ops/autograd_safe.py +120 -0
- diffbio/core/soft_ops/comparison.py +235 -0
- diffbio/core/soft_ops/elementwise.py +309 -0
- diffbio/core/soft_ops/logical.py +146 -0
- diffbio/core/soft_ops/quantile.py +376 -0
- diffbio/core/soft_ops/selection.py +236 -0
- diffbio/core/soft_ops/sorting.py +926 -0
- diffbio/core/soft_ops/straight_through.py +261 -0
- diffbio/core/uncertainty.py +279 -0
- diffbio/evaluation/__init__.py +42 -0
- diffbio/evaluation/adapters.py +409 -0
- diffbio/evaluation/graders.py +223 -0
- diffbio/evaluation/problem.py +157 -0
- diffbio/evaluation/runner.py +277 -0
- diffbio/losses/__init__.py +59 -0
- diffbio/losses/alignment_losses.py +222 -0
- diffbio/losses/biological_regularization.py +288 -0
- diffbio/losses/metric_losses.py +139 -0
- diffbio/losses/singlecell_losses.py +387 -0
- diffbio/losses/statistical_losses.py +345 -0
- diffbio/operators/__init__.py +60 -0
- diffbio/operators/_count_vae.py +197 -0
- diffbio/operators/_loss_balancing.py +65 -0
- diffbio/operators/_masked_gene_transformer.py +118 -0
- diffbio/operators/_transformer_validation.py +50 -0
- diffbio/operators/alignment/__init__.py +51 -0
- diffbio/operators/alignment/profile_hmm.py +350 -0
- diffbio/operators/alignment/scoring.py +127 -0
- diffbio/operators/alignment/smith_waterman.py +261 -0
- diffbio/operators/alignment/soft_msa.py +419 -0
- diffbio/operators/assembly/__init__.py +27 -0
- diffbio/operators/assembly/gnn_assembly.py +252 -0
- diffbio/operators/assembly/metagenomic_binning.py +296 -0
- diffbio/operators/crispr/__init__.py +17 -0
- diffbio/operators/crispr/guide_scoring.py +269 -0
- diffbio/operators/drug_discovery/__init__.py +133 -0
- diffbio/operators/drug_discovery/_graph_utils.py +142 -0
- diffbio/operators/drug_discovery/admet_predictor.py +285 -0
- diffbio/operators/drug_discovery/attentive_fp.py +411 -0
- diffbio/operators/drug_discovery/dti.py +261 -0
- diffbio/operators/drug_discovery/fingerprint.py +490 -0
- diffbio/operators/drug_discovery/maccs_keys.py +267 -0
- diffbio/operators/drug_discovery/message_passing.py +200 -0
- diffbio/operators/drug_discovery/primitives.py +242 -0
- diffbio/operators/drug_discovery/property_predictor.py +163 -0
- diffbio/operators/drug_discovery/similarity.py +193 -0
- diffbio/operators/epigenomics/__init__.py +35 -0
- diffbio/operators/epigenomics/chromatin_state.py +491 -0
- diffbio/operators/epigenomics/contextual.py +288 -0
- diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
- diffbio/operators/epigenomics/peak_calling.py +555 -0
- diffbio/operators/foundation_models/__init__.py +119 -0
- diffbio/operators/foundation_models/adapters.py +114 -0
- diffbio/operators/foundation_models/contracts.py +245 -0
- diffbio/operators/foundation_models/embedding_probe.py +83 -0
- diffbio/operators/foundation_models/experimental.py +128 -0
- diffbio/operators/foundation_models/foundation_model.py +332 -0
- diffbio/operators/foundation_models/frozen.py +59 -0
- diffbio/operators/foundation_models/precomputed.py +270 -0
- diffbio/operators/foundation_models/transformer_encoder.py +564 -0
- diffbio/operators/mapping/__init__.py +17 -0
- diffbio/operators/mapping/neural_mapper.py +493 -0
- diffbio/operators/metabolomics/__init__.py +39 -0
- diffbio/operators/metabolomics/spectral_similarity.py +315 -0
- diffbio/operators/molecular_dynamics/__init__.py +51 -0
- diffbio/operators/molecular_dynamics/force_field.py +265 -0
- diffbio/operators/molecular_dynamics/integrator.py +304 -0
- diffbio/operators/molecular_dynamics/primitives.py +115 -0
- diffbio/operators/multiomics/__init__.py +38 -0
- diffbio/operators/multiomics/hic_contact.py +377 -0
- diffbio/operators/multiomics/multiomics_vae.py +325 -0
- diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
- diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
- diffbio/operators/normalization/__init__.py +42 -0
- diffbio/operators/normalization/embedding.py +222 -0
- diffbio/operators/normalization/phate.py +400 -0
- diffbio/operators/normalization/umap.py +261 -0
- diffbio/operators/normalization/vae_normalizer.py +258 -0
- diffbio/operators/population/__init__.py +17 -0
- diffbio/operators/population/ancestry_estimation.py +274 -0
- diffbio/operators/preprocessing/__init__.py +76 -0
- diffbio/operators/preprocessing/adapter_removal.py +311 -0
- diffbio/operators/preprocessing/duplicate_filter.py +317 -0
- diffbio/operators/preprocessing/error_correction.py +287 -0
- diffbio/operators/protein/__init__.py +31 -0
- diffbio/operators/protein/secondary_structure.py +509 -0
- diffbio/operators/quality_filter.py +128 -0
- diffbio/operators/rna_structure/__init__.py +35 -0
- diffbio/operators/rna_structure/rna_folding.py +509 -0
- diffbio/operators/rnaseq/__init__.py +23 -0
- diffbio/operators/rnaseq/motif_discovery.py +251 -0
- diffbio/operators/rnaseq/splicing_psi.py +216 -0
- diffbio/operators/singlecell/__init__.py +193 -0
- diffbio/operators/singlecell/ambient_removal.py +333 -0
- diffbio/operators/singlecell/archetypes.py +191 -0
- diffbio/operators/singlecell/batch_correction.py +288 -0
- diffbio/operators/singlecell/cell_annotation.py +519 -0
- diffbio/operators/singlecell/communication.py +704 -0
- diffbio/operators/singlecell/differential_distribution.py +243 -0
- diffbio/operators/singlecell/doublet_detection.py +657 -0
- diffbio/operators/singlecell/downsampling.py +166 -0
- diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
- diffbio/operators/singlecell/grn_inference.py +336 -0
- diffbio/operators/singlecell/imputation.py +429 -0
- diffbio/operators/singlecell/knockdown_filter.py +176 -0
- diffbio/operators/singlecell/ot_trajectory.py +277 -0
- diffbio/operators/singlecell/simulation.py +444 -0
- diffbio/operators/singlecell/sindy_grn.py +247 -0
- diffbio/operators/singlecell/soft_clustering.py +211 -0
- diffbio/operators/singlecell/spatial_domains.py +677 -0
- diffbio/operators/singlecell/switch_de.py +184 -0
- diffbio/operators/singlecell/trajectory.py +447 -0
- diffbio/operators/singlecell/velocity.py +361 -0
- diffbio/operators/statistical/__init__.py +35 -0
- diffbio/operators/statistical/em_quantification.py +260 -0
- diffbio/operators/statistical/hmm.py +234 -0
- diffbio/operators/statistical/nb_glm.py +272 -0
- diffbio/operators/variant/__init__.py +64 -0
- diffbio/operators/variant/classifier.py +333 -0
- diffbio/operators/variant/cnn_classifier.py +255 -0
- diffbio/operators/variant/cnv_segmentation.py +678 -0
- diffbio/operators/variant/deepvariant_pileup.py +426 -0
- diffbio/operators/variant/pileup.py +240 -0
- diffbio/operators/variant/quality_recalibration.py +274 -0
- diffbio/pipelines/__init__.py +65 -0
- diffbio/pipelines/differential_expression.py +279 -0
- diffbio/pipelines/enhanced_variant_calling.py +326 -0
- diffbio/pipelines/perturbation.py +407 -0
- diffbio/pipelines/preprocessing.py +267 -0
- diffbio/pipelines/single_cell.py +366 -0
- diffbio/pipelines/variant_calling.py +490 -0
- diffbio/samplers/__init__.py +9 -0
- diffbio/samplers/perturbation_sampler.py +142 -0
- diffbio/sequences/__init__.py +34 -0
- diffbio/sequences/dna.py +239 -0
- diffbio/sources/__init__.py +149 -0
- diffbio/sources/_anndata_shared.py +89 -0
- diffbio/sources/_batch_iteration.py +37 -0
- diffbio/sources/_benchmark_source.py +152 -0
- diffbio/sources/_indexed_batch_source.py +38 -0
- diffbio/sources/_utils.py +45 -0
- diffbio/sources/anndata_interop.py +387 -0
- diffbio/sources/anndata_source.py +361 -0
- diffbio/sources/archive_ii.py +174 -0
- diffbio/sources/balifam.py +207 -0
- diffbio/sources/bam.py +265 -0
- diffbio/sources/bengrn_ground_truth.py +306 -0
- diffbio/sources/contextual_epigenomics.py +242 -0
- diffbio/sources/dti.py +359 -0
- diffbio/sources/embeddings.py +203 -0
- diffbio/sources/encode_peaks.py +223 -0
- diffbio/sources/fasta.py +226 -0
- diffbio/sources/immune_human.py +172 -0
- diffbio/sources/indexed_embeddings.py +128 -0
- diffbio/sources/indexed_view.py +191 -0
- diffbio/sources/molnet.py +493 -0
- diffbio/sources/multiomics.py +279 -0
- diffbio/sources/pancreas.py +108 -0
- diffbio/sources/perturbation/__init__.py +69 -0
- diffbio/sources/perturbation/_types.py +51 -0
- diffbio/sources/perturbation/_utils.py +125 -0
- diffbio/sources/perturbation/concat_source.py +115 -0
- diffbio/sources/perturbation/control_mapping.py +215 -0
- diffbio/sources/perturbation/experiment_config.py +261 -0
- diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
- diffbio/sources/perturbation/output_space.py +52 -0
- diffbio/sources/perturbation/perturbation_source.py +513 -0
- diffbio/sources/seqfish.py +145 -0
- diffbio/sources/sequence_foundation.py +68 -0
- diffbio/sources/singlecell_foundation.py +68 -0
- diffbio/splitters/__init__.py +63 -0
- diffbio/splitters/base.py +251 -0
- diffbio/splitters/molecular.py +330 -0
- diffbio/splitters/perturbation.py +199 -0
- diffbio/splitters/random.py +217 -0
- diffbio/splitters/sequence.py +201 -0
- diffbio/utils/__init__.py +55 -0
- diffbio/utils/dependency_runtime.py +115 -0
- diffbio/utils/nn_utils.py +157 -0
- diffbio/utils/quality.py +45 -0
- diffbio/utils/training.py +585 -0
- diffbio-0.1.0.dist-info/METADATA +480 -0
- diffbio-0.1.0.dist-info/RECORD +202 -0
- diffbio-0.1.0.dist-info/WHEEL +4 -0
- diffbio-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""ENCODE narrowPeak BED data source.
|
|
2
|
+
|
|
3
|
+
Loads peak calls from ENCODE narrowPeak BED files (gzipped or plain),
|
|
4
|
+
such as those produced by the ENCODE ChIP-seq pipeline. Each row in
|
|
5
|
+
the BED file encodes one called peak with:
|
|
6
|
+
|
|
7
|
+
chr start end name score strand signalValue pValue qValue peak
|
|
8
|
+
|
|
9
|
+
Only the first ten columns are required. The ``peak`` column (column 10)
|
|
10
|
+
gives the offset within the peak region to the summit position.
|
|
11
|
+
|
|
12
|
+
Peaks can be filtered to a single chromosome for speed and optionally
|
|
13
|
+
capped at a maximum count.
|
|
14
|
+
|
|
15
|
+
Reference:
|
|
16
|
+
ENCODE Project Consortium. "An integrated encyclopedia of DNA
|
|
17
|
+
elements in the human genome." Nature 489, 57-74 (2012).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import gzip
|
|
23
|
+
import logging
|
|
24
|
+
from collections.abc import Iterator
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
from datarax.core.config import StructuralConfig
|
|
31
|
+
from datarax.core.data_source import DataSourceModule
|
|
32
|
+
from flax import nnx
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
_DEFAULT_DATA_PATH = "/media/mahdi/ssd23/Data/encode/CTCF_K562_narrowPeak.bed.gz"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, kw_only=True)
|
|
40
|
+
class ENCODEPeakConfig(StructuralConfig):
|
|
41
|
+
"""Configuration for ENCODEPeakSource.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
data_path: Path to the narrowPeak BED file (gzipped or plain).
|
|
45
|
+
chromosome: Chromosome to filter peaks to. None loads all.
|
|
46
|
+
max_peaks: Maximum number of peaks to load. None loads all.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
data_path: str = _DEFAULT_DATA_PATH
|
|
50
|
+
chromosome: str | None = "chr22"
|
|
51
|
+
max_peaks: int | None = None
|
|
52
|
+
|
|
53
|
+
def __post_init__(self) -> None:
|
|
54
|
+
"""Validate that the data file exists."""
|
|
55
|
+
super().__post_init__()
|
|
56
|
+
path = Path(self.data_path)
|
|
57
|
+
if not path.exists():
|
|
58
|
+
raise FileNotFoundError(
|
|
59
|
+
f"ENCODE narrowPeak file not found: {path}. "
|
|
60
|
+
f"Download from https://www.encodeproject.org/"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, kw_only=True)
|
|
65
|
+
class ENCODEPeak:
|
|
66
|
+
"""A single ENCODE narrowPeak record.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
chromosome: Chromosome name (e.g. ``"chr22"``).
|
|
70
|
+
start: 0-based start coordinate.
|
|
71
|
+
end: 0-based end coordinate (exclusive).
|
|
72
|
+
signal_value: Fold-enrichment signal value.
|
|
73
|
+
p_value: -log10 p-value (or -1 if unavailable).
|
|
74
|
+
q_value: -log10 q-value (or -1 if unavailable).
|
|
75
|
+
summit_offset: Offset from ``start`` to the summit position.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
chromosome: str
|
|
79
|
+
start: int
|
|
80
|
+
end: int
|
|
81
|
+
signal_value: float
|
|
82
|
+
p_value: float
|
|
83
|
+
q_value: float
|
|
84
|
+
summit_offset: int
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _parse_narrowpeak(
|
|
88
|
+
file_path: Path,
|
|
89
|
+
chromosome: str | None,
|
|
90
|
+
max_peaks: int | None,
|
|
91
|
+
) -> list[ENCODEPeak]:
|
|
92
|
+
"""Parse a narrowPeak BED file into ENCODEPeak records.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
file_path: Path to gzipped or plain BED file.
|
|
96
|
+
chromosome: Filter to this chromosome. None keeps all.
|
|
97
|
+
max_peaks: Maximum peaks to return. None returns all.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Sorted list of ENCODEPeak records (by start position).
|
|
101
|
+
"""
|
|
102
|
+
peaks: list[ENCODEPeak] = []
|
|
103
|
+
opener = gzip.open if file_path.suffix == ".gz" else open
|
|
104
|
+
|
|
105
|
+
with opener(file_path, "rt", encoding="utf-8") as fh:
|
|
106
|
+
for line in fh:
|
|
107
|
+
line = line.strip()
|
|
108
|
+
if not line or line.startswith("#"):
|
|
109
|
+
continue
|
|
110
|
+
fields = line.split("\t")
|
|
111
|
+
if len(fields) < 10:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
chrom = fields[0]
|
|
115
|
+
if chromosome is not None and chrom != chromosome:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
peak = ENCODEPeak(
|
|
119
|
+
chromosome=chrom,
|
|
120
|
+
start=int(fields[1]),
|
|
121
|
+
end=int(fields[2]),
|
|
122
|
+
signal_value=float(fields[6]),
|
|
123
|
+
p_value=float(fields[7]),
|
|
124
|
+
q_value=float(fields[8]),
|
|
125
|
+
summit_offset=int(fields[9]),
|
|
126
|
+
)
|
|
127
|
+
peaks.append(peak)
|
|
128
|
+
|
|
129
|
+
if max_peaks is not None and len(peaks) >= max_peaks:
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
# Sort by genomic position for deterministic ordering
|
|
133
|
+
peaks.sort(key=lambda p: (p.chromosome, p.start))
|
|
134
|
+
return peaks
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ENCODEPeakSource(DataSourceModule):
|
|
138
|
+
"""DataSource for ENCODE narrowPeak ChIP-seq peak calls.
|
|
139
|
+
|
|
140
|
+
Loads peak positions and signal values from an ENCODE narrowPeak
|
|
141
|
+
BED file. Peaks are optionally filtered to a single chromosome
|
|
142
|
+
and/or capped at a maximum count.
|
|
143
|
+
|
|
144
|
+
Each loaded peak provides genomic coordinates, signal enrichment,
|
|
145
|
+
statistical significance, and summit position.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
```python
|
|
149
|
+
config = ENCODEPeakConfig(chromosome="chr22", max_peaks=500)
|
|
150
|
+
source = ENCODEPeakSource(config)
|
|
151
|
+
data = source.load()
|
|
152
|
+
print(data["n_peaks"]) # Number of peaks loaded
|
|
153
|
+
print(data["starts"][:5]) # First 5 start positions
|
|
154
|
+
```
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
data: dict[str, Any] = nnx.data()
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
config: ENCODEPeakConfig,
|
|
162
|
+
*,
|
|
163
|
+
rngs: nnx.Rngs | None = None,
|
|
164
|
+
name: str | None = None,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Load ENCODE narrowPeak data from BED file.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
config: Configuration with file path and filters.
|
|
170
|
+
rngs: Optional RNG state (unused, for interface compat).
|
|
171
|
+
name: Optional module name.
|
|
172
|
+
"""
|
|
173
|
+
super().__init__(config, rngs=rngs, name=name or "ENCODEPeakSource")
|
|
174
|
+
file_path = Path(config.data_path)
|
|
175
|
+
peaks = _parse_narrowpeak(file_path, config.chromosome, config.max_peaks)
|
|
176
|
+
|
|
177
|
+
if not peaks:
|
|
178
|
+
chrom_msg = f" on {config.chromosome}" if config.chromosome else ""
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"No peaks found in {file_path}{chrom_msg}. "
|
|
181
|
+
f"Check the file format and chromosome filter."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
starts = np.array([p.start for p in peaks], dtype=np.int64)
|
|
185
|
+
ends = np.array([p.end for p in peaks], dtype=np.int64)
|
|
186
|
+
signals = np.array([p.signal_value for p in peaks], dtype=np.float64)
|
|
187
|
+
summits = np.array(
|
|
188
|
+
[p.start + p.summit_offset for p in peaks],
|
|
189
|
+
dtype=np.int64,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self.data = {
|
|
193
|
+
"peaks": peaks,
|
|
194
|
+
"starts": starts,
|
|
195
|
+
"ends": ends,
|
|
196
|
+
"signal_values": signals,
|
|
197
|
+
"summit_positions": summits,
|
|
198
|
+
"n_peaks": len(peaks),
|
|
199
|
+
"chromosome": config.chromosome,
|
|
200
|
+
}
|
|
201
|
+
logger.info(
|
|
202
|
+
"Loaded ENCODE peaks: %d peaks from %s%s",
|
|
203
|
+
len(peaks),
|
|
204
|
+
file_path.name,
|
|
205
|
+
f" ({config.chromosome})" if config.chromosome else "",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def load(self) -> dict[str, Any]:
|
|
209
|
+
"""Return the full dataset as a dictionary.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Dict with keys: peaks, starts, ends, signal_values,
|
|
213
|
+
summit_positions, n_peaks, chromosome.
|
|
214
|
+
"""
|
|
215
|
+
return self.data
|
|
216
|
+
|
|
217
|
+
def __len__(self) -> int:
|
|
218
|
+
"""Return the number of loaded peaks."""
|
|
219
|
+
return self.data["n_peaks"]
|
|
220
|
+
|
|
221
|
+
def __iter__(self) -> Iterator[ENCODEPeak]:
|
|
222
|
+
"""Iterate over individual peak records."""
|
|
223
|
+
yield from self.data["peaks"]
|
diffbio/sources/fasta.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""FASTA file data source for genomics workflows.
|
|
2
|
+
|
|
3
|
+
This module provides FastaSource for reading DNA/RNA sequences
|
|
4
|
+
from FASTA files with lazy loading and efficient indexed access.
|
|
5
|
+
|
|
6
|
+
Based on best practices from:
|
|
7
|
+
- pyfaidx (samtools-compatible FASTA indexing)
|
|
8
|
+
- BioPython SeqIO.index patterns
|
|
9
|
+
- Google Nucleus FASTA handling
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
- https://github.com/mdshw5/pyfaidx
|
|
13
|
+
- https://pythonhosted.org/pyfaidx/
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from collections.abc import Iterator
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
from flax import nnx
|
|
23
|
+
|
|
24
|
+
from datarax.core.config import StructuralConfig
|
|
25
|
+
from datarax.core.data_source import DataSourceModule
|
|
26
|
+
from datarax.typing import Element
|
|
27
|
+
|
|
28
|
+
from diffbio.sequences.dna import encode_dna_string
|
|
29
|
+
from diffbio.sources._indexed_batch_source import IndexedBatchSourceMixin
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class FastaSourceConfig(StructuralConfig):
|
|
36
|
+
"""Configuration for FASTA data source.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
file_path: Path to FASTA file
|
|
40
|
+
handle_n: How to handle N nucleotides ("uniform" or "zero")
|
|
41
|
+
create_index: Whether to create .fai index if not exists (default: True)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
file_path: Path = None # type: ignore[assignment] # Required, validated in post_init
|
|
45
|
+
handle_n: Literal["uniform", "zero"] = "uniform"
|
|
46
|
+
create_index: bool = True
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
"""Validate configuration after initialization."""
|
|
50
|
+
super().__post_init__()
|
|
51
|
+
if self.file_path is None:
|
|
52
|
+
raise ValueError("file_path is required")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FastaSource(IndexedBatchSourceMixin, DataSourceModule):
|
|
56
|
+
"""FASTA file data source extending Datarax DataSourceModule.
|
|
57
|
+
|
|
58
|
+
Provides efficient access to DNA/RNA sequences with:
|
|
59
|
+
|
|
60
|
+
- Lazy loading using samtools-compatible .fai index
|
|
61
|
+
- Dictionary-like access by sequence name
|
|
62
|
+
- One-hot encoded sequence output
|
|
63
|
+
- Support for compressed BGZF files
|
|
64
|
+
|
|
65
|
+
Inherits from DataSourceModule (StructuralModule) because:
|
|
66
|
+
|
|
67
|
+
- Non-parametric: FASTA reading is deterministic
|
|
68
|
+
- Frozen config: file parameters don't change
|
|
69
|
+
- Domain-specific: requires genomics-specific handling
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
```python
|
|
73
|
+
config = FastaSourceConfig(file_path=Path("genome.fasta"))
|
|
74
|
+
source = FastaSource(config)
|
|
75
|
+
elem = source.get_by_name("chr1")
|
|
76
|
+
print(elem.data["sequence"].shape)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Performance Tips (from pyfaidx best practices):
|
|
80
|
+
|
|
81
|
+
- Use indexed FASTA files (.fai) for random access
|
|
82
|
+
- Access regions with slicing for large chromosomes
|
|
83
|
+
- BGZF compression reduces disk space while maintaining random access
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
# Annotate data storage for Flax NNX
|
|
87
|
+
_sequence_names: list = nnx.data()
|
|
88
|
+
_name_to_idx: dict = nnx.data()
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
config: FastaSourceConfig,
|
|
93
|
+
*,
|
|
94
|
+
rngs: nnx.Rngs | None = None,
|
|
95
|
+
name: str | None = None,
|
|
96
|
+
):
|
|
97
|
+
"""Initialize FastaSource.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
config: FASTA source configuration
|
|
101
|
+
rngs: Random number generators (unused for data loading)
|
|
102
|
+
name: Optional module name
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
FileNotFoundError: If FASTA file not found
|
|
106
|
+
ImportError: If pyfaidx is not installed
|
|
107
|
+
"""
|
|
108
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
109
|
+
|
|
110
|
+
# Import pyfaidx lazily to allow installation without it
|
|
111
|
+
try:
|
|
112
|
+
import pyfaidx
|
|
113
|
+
|
|
114
|
+
self._pyfaidx = pyfaidx
|
|
115
|
+
except ImportError as err:
|
|
116
|
+
raise ImportError(
|
|
117
|
+
"pyfaidx is required for FastaSource. Install with: pip install pyfaidx"
|
|
118
|
+
) from err
|
|
119
|
+
|
|
120
|
+
# Validate file exists
|
|
121
|
+
if not config.file_path.exists():
|
|
122
|
+
raise FileNotFoundError(f"FASTA file not found: {config.file_path}")
|
|
123
|
+
|
|
124
|
+
# Open FASTA file with pyfaidx (creates index if needed)
|
|
125
|
+
self._fasta = self._pyfaidx.Fasta(
|
|
126
|
+
str(config.file_path),
|
|
127
|
+
build_index=config.create_index,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Build sequence name index
|
|
131
|
+
self._sequence_names = list(self._fasta.keys())
|
|
132
|
+
self._name_to_idx = {name: idx for idx, name in enumerate(self._sequence_names)}
|
|
133
|
+
self._current_idx = 0
|
|
134
|
+
|
|
135
|
+
def _sequence_to_element(self, idx: int, seq_name: str) -> Element:
|
|
136
|
+
"""Convert FASTA sequence to Element with one-hot encoding.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
idx: Index of the sequence
|
|
140
|
+
seq_name: Name/ID of the sequence
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Element with one-hot encoded sequence and metadata
|
|
144
|
+
"""
|
|
145
|
+
# Get sequence from pyfaidx (lazy loaded)
|
|
146
|
+
fasta_seq = self._fasta[seq_name]
|
|
147
|
+
sequence_str = str(fasta_seq).upper()
|
|
148
|
+
|
|
149
|
+
# Encode sequence as one-hot
|
|
150
|
+
sequence = encode_dna_string(sequence_str, handle_n=self.config.handle_n)
|
|
151
|
+
|
|
152
|
+
# Get description if available
|
|
153
|
+
description = getattr(fasta_seq, "long_name", seq_name)
|
|
154
|
+
|
|
155
|
+
data = {
|
|
156
|
+
"sequence": sequence,
|
|
157
|
+
"sequence_id": seq_name,
|
|
158
|
+
"description": description,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
metadata = {
|
|
162
|
+
"idx": idx,
|
|
163
|
+
"length": len(sequence_str),
|
|
164
|
+
"file_path": str(self.config.file_path),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return Element(data=data, state={}, metadata=metadata) # pyright: ignore[reportArgumentType]
|
|
168
|
+
|
|
169
|
+
def __len__(self) -> int:
|
|
170
|
+
"""Return the number of sequences in the source."""
|
|
171
|
+
return len(self._sequence_names)
|
|
172
|
+
|
|
173
|
+
def __getitem__(self, idx: int) -> Element | None:
|
|
174
|
+
"""Get sequence by index.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
idx: Index of the sequence
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Element at the given index, or None if out of bounds
|
|
181
|
+
"""
|
|
182
|
+
if idx < 0 or idx >= len(self._sequence_names):
|
|
183
|
+
return None
|
|
184
|
+
seq_name = self._sequence_names[idx]
|
|
185
|
+
return self._sequence_to_element(idx, seq_name)
|
|
186
|
+
|
|
187
|
+
def __iter__(self) -> Iterator[Element]: # type: ignore[override]
|
|
188
|
+
"""Return iterator over sequences."""
|
|
189
|
+
self._current_idx = 0
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def __next__(self) -> Element:
|
|
193
|
+
"""Get next sequence in iteration."""
|
|
194
|
+
if self._current_idx >= len(self._sequence_names):
|
|
195
|
+
raise StopIteration
|
|
196
|
+
seq_name = self._sequence_names[self._current_idx]
|
|
197
|
+
elem = self._sequence_to_element(self._current_idx, seq_name)
|
|
198
|
+
self._current_idx += 1
|
|
199
|
+
return elem
|
|
200
|
+
|
|
201
|
+
def _batch_total_size(self) -> int:
|
|
202
|
+
"""Return number of indexed sequences for mixin batch iteration."""
|
|
203
|
+
return len(self._sequence_names)
|
|
204
|
+
|
|
205
|
+
def _batch_element(self, idx: int) -> Element:
|
|
206
|
+
"""Build the indexed sequence element for mixin batch iteration."""
|
|
207
|
+
return self._sequence_to_element(idx, self._sequence_names[idx])
|
|
208
|
+
|
|
209
|
+
def get_by_name(self, name: str) -> Element | None:
|
|
210
|
+
"""Get sequence by name/ID.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
name: Sequence identifier (e.g., "chr1", "seq1")
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Element for the sequence, or None if not found
|
|
217
|
+
"""
|
|
218
|
+
if name not in self._name_to_idx:
|
|
219
|
+
return None
|
|
220
|
+
idx = self._name_to_idx[name]
|
|
221
|
+
return self._sequence_to_element(idx, name)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def sequence_names(self) -> list[str]:
|
|
225
|
+
"""Get list of all sequence names in the FASTA file."""
|
|
226
|
+
return list(self._sequence_names)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Immune human integration benchmark DataSource.
|
|
2
|
+
|
|
3
|
+
Loads the human immune cell atlas dataset from the scib benchmark
|
|
4
|
+
(Luecken et al., Nature Methods 2022). This is the standard dataset
|
|
5
|
+
for evaluating single-cell batch integration methods.
|
|
6
|
+
|
|
7
|
+
Dataset: 33,506 cells, 12,303 genes, 10 batches, 16 cell types.
|
|
8
|
+
Source: https://figshare.com/articles/dataset/12420968
|
|
9
|
+
|
|
10
|
+
The dataset must be pre-downloaded to a local directory. See
|
|
11
|
+
``benchmarks/README.md`` for download instructions.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import jax.numpy as jnp
|
|
22
|
+
import numpy as np
|
|
23
|
+
from flax import nnx
|
|
24
|
+
|
|
25
|
+
from datarax.core.config import StructuralConfig
|
|
26
|
+
|
|
27
|
+
from diffbio.sources._benchmark_source import (
|
|
28
|
+
BenchmarkDataSource,
|
|
29
|
+
encode_label_column,
|
|
30
|
+
)
|
|
31
|
+
from diffbio.sources._utils import to_dense_float32 as _to_dense
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
_FILENAME = "Immune_ALL_human.h5ad"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, kw_only=True)
|
|
39
|
+
class ImmuneHumanConfig(StructuralConfig):
|
|
40
|
+
"""Configuration for ImmuneHumanSource.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
data_dir: Directory containing the downloaded h5ad file.
|
|
44
|
+
subsample: If set, randomly subsample this many cells.
|
|
45
|
+
Use for quick/CI benchmark runs.
|
|
46
|
+
batch_key: Column name in obs for batch labels.
|
|
47
|
+
label_key: Column name in obs for cell type labels.
|
|
48
|
+
embedding_key: Key in obsm for precomputed embeddings.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
data_dir: str = "/media/mahdi/ssd23/Data/scib"
|
|
52
|
+
subsample: int | None = None
|
|
53
|
+
batch_key: str = "batch"
|
|
54
|
+
label_key: str = "final_annotation"
|
|
55
|
+
embedding_key: str = "X_pca"
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
"""Validate configuration."""
|
|
59
|
+
super().__post_init__()
|
|
60
|
+
path = Path(self.data_dir) / _FILENAME
|
|
61
|
+
if not path.exists():
|
|
62
|
+
raise FileNotFoundError(
|
|
63
|
+
f"Dataset not found: {path}. "
|
|
64
|
+
f"Download from: https://ndownloader.figshare.com/"
|
|
65
|
+
f"files/25717328"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ImmuneHumanSource(BenchmarkDataSource):
|
|
70
|
+
"""DataSource for the scib immune human integration benchmark.
|
|
71
|
+
|
|
72
|
+
Loads the human immune cell atlas (33,506 cells, 12,303 genes,
|
|
73
|
+
10 batches, 16 cell types) from a pre-downloaded h5ad file.
|
|
74
|
+
|
|
75
|
+
Follows the datarax DataSourceModule pattern: eager loading at
|
|
76
|
+
init, dict-based access via ``load()``, length via ``__len__``.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
```python
|
|
80
|
+
config = ImmuneHumanConfig(data_dir="/path/to/data")
|
|
81
|
+
source = ImmuneHumanSource(config)
|
|
82
|
+
data = source.load()
|
|
83
|
+
print(data["counts"].shape) # (33506, 12303)
|
|
84
|
+
```
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
config: ImmuneHumanConfig,
|
|
90
|
+
*,
|
|
91
|
+
rngs: nnx.Rngs | None = None,
|
|
92
|
+
name: str | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Load the immune human dataset.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
config: Configuration with data directory and options.
|
|
98
|
+
rngs: Optional RNG state (unused, for interface compat).
|
|
99
|
+
name: Optional module name.
|
|
100
|
+
"""
|
|
101
|
+
super().__init__(config, rngs=rngs, name=name or "ImmuneHumanSource")
|
|
102
|
+
self.data = self._load(config)
|
|
103
|
+
self._log_loaded_summary(
|
|
104
|
+
logger,
|
|
105
|
+
"immune_human",
|
|
106
|
+
("n_cells", "n_genes", "n_batches", "n_types"),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _load(self, config: ImmuneHumanConfig) -> dict[str, Any]:
|
|
110
|
+
"""Load and preprocess the h5ad file."""
|
|
111
|
+
adata, counts = self._load_benchmark_counts(config, _FILENAME, _to_dense)
|
|
112
|
+
|
|
113
|
+
# Encode categorical labels as integer codes
|
|
114
|
+
batch_labels = encode_label_column(adata.obs[config.batch_key])
|
|
115
|
+
cell_type_labels = encode_label_column(adata.obs[config.label_key])
|
|
116
|
+
|
|
117
|
+
# Get embeddings (PCA from obsm, or compute on the fly)
|
|
118
|
+
if config.embedding_key in adata.obsm:
|
|
119
|
+
embeddings = jnp.array(np.asarray(adata.obsm[config.embedding_key], dtype=np.float32))
|
|
120
|
+
else:
|
|
121
|
+
logger.info(
|
|
122
|
+
"Embedding key '%s' not in obsm (%s). Computing PCA (50 components).",
|
|
123
|
+
config.embedding_key,
|
|
124
|
+
list(adata.obsm.keys()),
|
|
125
|
+
)
|
|
126
|
+
embeddings = self._compute_pca(counts, n_components=50)
|
|
127
|
+
|
|
128
|
+
gene_names = list(adata.var_names)
|
|
129
|
+
cell_ids = [str(cell_id) for cell_id in adata.obs_names]
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"counts": counts,
|
|
133
|
+
"batch_labels": batch_labels,
|
|
134
|
+
"cell_type_labels": cell_type_labels,
|
|
135
|
+
"cell_ids": cell_ids,
|
|
136
|
+
"embeddings": embeddings,
|
|
137
|
+
"gene_names": gene_names,
|
|
138
|
+
"n_cells": adata.n_obs,
|
|
139
|
+
"n_genes": adata.n_vars,
|
|
140
|
+
"n_batches": int(len(np.unique(batch_labels))),
|
|
141
|
+
"n_types": int(len(np.unique(cell_type_labels))),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _compute_pca(counts: jnp.ndarray, n_components: int = 50) -> jnp.ndarray:
|
|
146
|
+
"""Compute PCA embeddings from count matrix.
|
|
147
|
+
|
|
148
|
+
Log-normalizes, then computes truncated SVD for PCA.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
counts: Dense count matrix (n_cells, n_genes).
|
|
152
|
+
n_components: Number of principal components.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
PCA embeddings of shape (n_cells, n_components).
|
|
156
|
+
"""
|
|
157
|
+
# Log-normalize: log1p(counts / total * 10000)
|
|
158
|
+
totals = jnp.sum(counts, axis=1, keepdims=True)
|
|
159
|
+
totals = jnp.maximum(totals, 1.0)
|
|
160
|
+
normalized = jnp.log1p(counts / totals * 10000.0)
|
|
161
|
+
|
|
162
|
+
# Center
|
|
163
|
+
mean = jnp.mean(normalized, axis=0)
|
|
164
|
+
centered = normalized - mean
|
|
165
|
+
|
|
166
|
+
# Truncated SVD via numpy (JAX full SVD on 33K x 12K is expensive)
|
|
167
|
+
centered_np = np.asarray(centered)
|
|
168
|
+
from sklearn.decomposition import TruncatedSVD # noqa: PLC0415
|
|
169
|
+
|
|
170
|
+
svd = TruncatedSVD(n_components=n_components, random_state=42)
|
|
171
|
+
embeddings = svd.fit_transform(centered_np)
|
|
172
|
+
return jnp.array(embeddings.astype(np.float32))
|