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,207 @@
|
|
|
1
|
+
"""BAliBASE reference alignment DataSource (balifam).
|
|
2
|
+
|
|
3
|
+
Loads protein families from the balifam repository, which provides
|
|
4
|
+
curated subsets of BAliBASE reference alignments at three tiers
|
|
5
|
+
(100, 1000, 10000 sequences per family).
|
|
6
|
+
|
|
7
|
+
Each family contains unaligned input sequences and a reference
|
|
8
|
+
alignment with mixed-case convention:
|
|
9
|
+
- Uppercase residues: core (scored) positions
|
|
10
|
+
- Lowercase residues: insert (unscored) positions
|
|
11
|
+
- Dots (.): gap characters
|
|
12
|
+
|
|
13
|
+
The dataset must be available locally. See:
|
|
14
|
+
https://github.com/steineggerlab/balifam
|
|
15
|
+
|
|
16
|
+
Data directory layout::
|
|
17
|
+
|
|
18
|
+
balifam100/
|
|
19
|
+
in/ # Unaligned FASTA files (one per family)
|
|
20
|
+
ref/ # Reference alignments (FASTA with gaps)
|
|
21
|
+
info/ # Family ID lists
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from collections.abc import Iterator
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from datarax.core.config import StructuralConfig
|
|
33
|
+
from datarax.core.data_source import DataSourceModule
|
|
34
|
+
from flax import nnx
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
_DEFAULT_DATA_DIR = "/media/mahdi/ssd23/Works/balifam"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_fasta(path: Path) -> list[tuple[str, str]]:
|
|
42
|
+
"""Parse a FASTA file into (name, sequence) tuples.
|
|
43
|
+
|
|
44
|
+
Handles multi-line sequences by concatenating continuation lines.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path: Path to FASTA file.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of (sequence_name, sequence_string) tuples.
|
|
51
|
+
"""
|
|
52
|
+
entries: list[tuple[str, str]] = []
|
|
53
|
+
current_name: str | None = None
|
|
54
|
+
current_seq_parts: list[str] = []
|
|
55
|
+
|
|
56
|
+
with path.open() as fh:
|
|
57
|
+
for line in fh:
|
|
58
|
+
line = line.rstrip("\n")
|
|
59
|
+
if line.startswith(">"):
|
|
60
|
+
if current_name is not None:
|
|
61
|
+
entries.append((current_name, "".join(current_seq_parts)))
|
|
62
|
+
current_name = line[1:].strip()
|
|
63
|
+
current_seq_parts = []
|
|
64
|
+
elif current_name is not None:
|
|
65
|
+
current_seq_parts.append(line)
|
|
66
|
+
|
|
67
|
+
if current_name is not None:
|
|
68
|
+
entries.append((current_name, "".join(current_seq_parts)))
|
|
69
|
+
|
|
70
|
+
return entries
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, kw_only=True)
|
|
74
|
+
class BalifamConfig(StructuralConfig):
|
|
75
|
+
"""Configuration for BalifamSource.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
data_dir: Root directory of the balifam repository.
|
|
79
|
+
tier: Family size tier (100, 1000, or 10000 sequences).
|
|
80
|
+
max_families: Maximum number of families to load.
|
|
81
|
+
None loads all available families.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
data_dir: str = _DEFAULT_DATA_DIR
|
|
85
|
+
tier: int = 100
|
|
86
|
+
max_families: int | None = None
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
"""Validate configuration."""
|
|
90
|
+
super().__post_init__()
|
|
91
|
+
valid_tiers = {100, 1000, 10000}
|
|
92
|
+
if self.tier not in valid_tiers:
|
|
93
|
+
raise ValueError(f"tier must be one of {valid_tiers}, got {self.tier}")
|
|
94
|
+
tier_dir = Path(self.data_dir) / f"balifam{self.tier}"
|
|
95
|
+
if not tier_dir.exists():
|
|
96
|
+
raise FileNotFoundError(
|
|
97
|
+
f"Balifam tier directory not found: {tier_dir}. "
|
|
98
|
+
f"Clone from: https://github.com/steineggerlab/balifam"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class BalifamSource(DataSourceModule):
|
|
103
|
+
"""DataSource for BAliBASE reference alignments (balifam).
|
|
104
|
+
|
|
105
|
+
Loads protein family alignments from balifam for evaluating
|
|
106
|
+
multiple sequence alignment methods. Each family provides
|
|
107
|
+
unaligned input sequences and a curated reference alignment.
|
|
108
|
+
|
|
109
|
+
The reference alignment contains a subset of the input sequences
|
|
110
|
+
with known structural alignment, using mixed-case annotation
|
|
111
|
+
(uppercase = core/scored, lowercase = insert/unscored).
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
```python
|
|
115
|
+
config = BalifamConfig(tier=100, max_families=5)
|
|
116
|
+
source = BalifamSource(config)
|
|
117
|
+
families = source.load()
|
|
118
|
+
print(families[0]["family_id"])
|
|
119
|
+
```
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
families: list[dict[str, Any]] = nnx.data()
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
config: BalifamConfig,
|
|
127
|
+
*,
|
|
128
|
+
rngs: nnx.Rngs | None = None,
|
|
129
|
+
name: str | None = None,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Load balifam families.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
config: Configuration with data directory and options.
|
|
135
|
+
rngs: Optional RNG state (unused, for interface compat).
|
|
136
|
+
name: Optional module name.
|
|
137
|
+
"""
|
|
138
|
+
super().__init__(config, rngs=rngs, name=name or "BalifamSource")
|
|
139
|
+
self.families = self._load_families(config)
|
|
140
|
+
logger.info(
|
|
141
|
+
"Loaded %d balifam%d families",
|
|
142
|
+
len(self.families),
|
|
143
|
+
config.tier,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _load_families(self, config: BalifamConfig) -> list[dict[str, Any]]:
|
|
147
|
+
"""Load families from disk.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
config: Source configuration.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of family dicts with keys: family_id, sequences,
|
|
154
|
+
reference, n_sequences, n_reference.
|
|
155
|
+
"""
|
|
156
|
+
tier_dir = Path(config.data_dir) / f"balifam{config.tier}"
|
|
157
|
+
in_dir = tier_dir / "in"
|
|
158
|
+
ref_dir = tier_dir / "ref"
|
|
159
|
+
|
|
160
|
+
# Discover families from reference directory (authoritative)
|
|
161
|
+
family_files = sorted(ref_dir.iterdir())
|
|
162
|
+
if config.max_families is not None:
|
|
163
|
+
family_files = family_files[: config.max_families]
|
|
164
|
+
|
|
165
|
+
families: list[dict[str, Any]] = []
|
|
166
|
+
for ref_path in family_files:
|
|
167
|
+
family_id = ref_path.name
|
|
168
|
+
in_path = in_dir / family_id
|
|
169
|
+
|
|
170
|
+
if not in_path.exists():
|
|
171
|
+
logger.warning(
|
|
172
|
+
"Input file missing for family %s, skipping",
|
|
173
|
+
family_id,
|
|
174
|
+
)
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
sequences = _parse_fasta(in_path)
|
|
178
|
+
reference = _parse_fasta(ref_path)
|
|
179
|
+
|
|
180
|
+
families.append(
|
|
181
|
+
{
|
|
182
|
+
"family_id": family_id,
|
|
183
|
+
"sequences": sequences,
|
|
184
|
+
"reference": reference,
|
|
185
|
+
"n_sequences": len(sequences),
|
|
186
|
+
"n_reference": len(reference),
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return families
|
|
191
|
+
|
|
192
|
+
def load(self) -> list[dict[str, Any]]:
|
|
193
|
+
"""Return all loaded families.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of family dicts, each with keys: family_id,
|
|
197
|
+
sequences, reference, n_sequences, n_reference.
|
|
198
|
+
"""
|
|
199
|
+
return self.families
|
|
200
|
+
|
|
201
|
+
def __len__(self) -> int:
|
|
202
|
+
"""Return the number of loaded families."""
|
|
203
|
+
return len(self.families)
|
|
204
|
+
|
|
205
|
+
def __iter__(self) -> Iterator[dict[str, Any]]:
|
|
206
|
+
"""Iterate over families."""
|
|
207
|
+
return iter(self.families)
|
diffbio/sources/bam.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""BAM/CRAM file data source for genomics workflows.
|
|
2
|
+
|
|
3
|
+
This module provides BAMSource for reading aligned sequencing reads
|
|
4
|
+
from BAM/CRAM files with lazy loading and efficient indexed access.
|
|
5
|
+
|
|
6
|
+
Based on best practices from:
|
|
7
|
+
- pysam (HTSlib Python wrapper)
|
|
8
|
+
- Google Nucleus genomics library
|
|
9
|
+
- DeepVariant BAM handling patterns
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
- https://pysam.readthedocs.io/
|
|
13
|
+
- https://github.com/pysam-developers/pysam
|
|
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
|
+
import jax.numpy as jnp
|
|
23
|
+
from flax import nnx
|
|
24
|
+
|
|
25
|
+
from datarax.core.config import StructuralConfig
|
|
26
|
+
from datarax.core.data_source import DataSourceModule
|
|
27
|
+
from datarax.typing import Element
|
|
28
|
+
|
|
29
|
+
from diffbio.sequences.dna import encode_dna_string
|
|
30
|
+
from diffbio.sources._indexed_batch_source import IndexedBatchSourceMixin
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class BAMSourceConfig(StructuralConfig):
|
|
37
|
+
"""Configuration for BAM/CRAM data source.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
file_path: Path to BAM/CRAM file
|
|
41
|
+
reference_path: Optional path to reference FASTA (required for CRAM)
|
|
42
|
+
include_unmapped: Whether to include unmapped reads (default: False)
|
|
43
|
+
min_mapping_quality: Minimum mapping quality to include (default: None)
|
|
44
|
+
region: Optional genomic region to query (e.g., "chr1:1000-2000")
|
|
45
|
+
handle_n: How to handle N nucleotides in sequences
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
file_path: Path = None # type: ignore[assignment] # Required, validated in post_init
|
|
49
|
+
reference_path: Path | None = None
|
|
50
|
+
include_unmapped: bool = False
|
|
51
|
+
min_mapping_quality: int | None = None
|
|
52
|
+
region: str | None = None
|
|
53
|
+
handle_n: Literal["uniform", "zero"] = "uniform"
|
|
54
|
+
|
|
55
|
+
def __post_init__(self) -> None:
|
|
56
|
+
"""Validate configuration after initialization."""
|
|
57
|
+
super().__post_init__()
|
|
58
|
+
if self.file_path is None:
|
|
59
|
+
raise ValueError("file_path is required")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BAMSource(IndexedBatchSourceMixin, DataSourceModule):
|
|
63
|
+
"""BAM/CRAM file data source extending Datarax DataSourceModule.
|
|
64
|
+
|
|
65
|
+
Provides efficient access to aligned sequencing reads with:
|
|
66
|
+
|
|
67
|
+
- Lazy loading using pysam iterators
|
|
68
|
+
- Indexed random access via BAI/CRAI files
|
|
69
|
+
- Quality filtering at load time
|
|
70
|
+
- One-hot encoded sequence output
|
|
71
|
+
|
|
72
|
+
Inherits from DataSourceModule (StructuralModule) because:
|
|
73
|
+
|
|
74
|
+
- Non-parametric: BAM reading is deterministic
|
|
75
|
+
- Frozen config: file parameters don't change
|
|
76
|
+
- Domain-specific: requires genomics-specific handling
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
```python
|
|
80
|
+
config = BAMSourceConfig(file_path=Path("sample.bam"))
|
|
81
|
+
source = BAMSource(config)
|
|
82
|
+
for element in source:
|
|
83
|
+
print(element.data["read_name"], element.data["sequence"].shape)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Performance Tips (from pysam best practices):
|
|
87
|
+
|
|
88
|
+
- Use indexed BAM files for random access
|
|
89
|
+
- Filter by region to reduce data loading
|
|
90
|
+
- Set min_mapping_quality to filter at read time
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
# Annotate data storage for Flax NNX
|
|
94
|
+
_reads: list = nnx.data()
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
config: BAMSourceConfig,
|
|
99
|
+
*,
|
|
100
|
+
rngs: nnx.Rngs | None = None,
|
|
101
|
+
name: str | None = None,
|
|
102
|
+
):
|
|
103
|
+
"""Initialize BAMSource.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config: BAM source configuration
|
|
107
|
+
rngs: Random number generators (unused for data loading)
|
|
108
|
+
name: Optional module name
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
FileNotFoundError: If BAM file not found
|
|
112
|
+
ImportError: If pysam is not installed
|
|
113
|
+
"""
|
|
114
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
115
|
+
|
|
116
|
+
# Import pysam lazily to allow installation without it
|
|
117
|
+
try:
|
|
118
|
+
import pysam
|
|
119
|
+
|
|
120
|
+
self._pysam = pysam
|
|
121
|
+
except ImportError as err:
|
|
122
|
+
raise ImportError(
|
|
123
|
+
"pysam is required for BAMSource. Install with: pip install pysam"
|
|
124
|
+
) from err
|
|
125
|
+
|
|
126
|
+
# Validate file exists
|
|
127
|
+
if not config.file_path.exists():
|
|
128
|
+
raise FileNotFoundError(f"BAM file not found: {config.file_path}")
|
|
129
|
+
|
|
130
|
+
# Load read index (metadata only, not full sequences)
|
|
131
|
+
self._reads = self._index_reads()
|
|
132
|
+
self._current_idx = 0
|
|
133
|
+
|
|
134
|
+
def _index_reads(self) -> list[dict]:
|
|
135
|
+
"""Build an index of reads for random access.
|
|
136
|
+
|
|
137
|
+
This loads read metadata without fully parsing sequences,
|
|
138
|
+
enabling lazy loading on access.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of read metadata dictionaries
|
|
142
|
+
"""
|
|
143
|
+
config = self.config
|
|
144
|
+
reads: list[dict] = []
|
|
145
|
+
|
|
146
|
+
# Open BAM file
|
|
147
|
+
mode = "rb" if str(config.file_path).endswith(".bam") else "rc"
|
|
148
|
+
reference = str(config.reference_path) if config.reference_path else None
|
|
149
|
+
|
|
150
|
+
with self._pysam.AlignmentFile(
|
|
151
|
+
str(config.file_path), mode, reference_filename=reference
|
|
152
|
+
) as bam:
|
|
153
|
+
for read in self._iter_reads(bam):
|
|
154
|
+
if self._should_skip_read(read):
|
|
155
|
+
continue
|
|
156
|
+
reads.append(self._read_info(read))
|
|
157
|
+
|
|
158
|
+
return reads
|
|
159
|
+
|
|
160
|
+
def _iter_reads(self, bam) -> object:
|
|
161
|
+
"""Return the configured BAM iterator."""
|
|
162
|
+
if self.config.region:
|
|
163
|
+
return bam.fetch(region=self.config.region)
|
|
164
|
+
return bam.fetch(until_eof=True)
|
|
165
|
+
|
|
166
|
+
def _should_skip_read(self, read) -> bool:
|
|
167
|
+
"""Check whether a read should be excluded by source filters."""
|
|
168
|
+
if read.is_unmapped and not self.config.include_unmapped:
|
|
169
|
+
return True
|
|
170
|
+
min_mapq = self.config.min_mapping_quality
|
|
171
|
+
return min_mapq is not None and read.mapping_quality < min_mapq
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _read_info(read) -> dict[str, object]:
|
|
175
|
+
"""Extract index metadata from a pysam read."""
|
|
176
|
+
return {
|
|
177
|
+
"query_name": read.query_name,
|
|
178
|
+
"reference_id": read.reference_id,
|
|
179
|
+
"reference_start": read.reference_start,
|
|
180
|
+
"is_unmapped": read.is_unmapped,
|
|
181
|
+
"query_sequence": read.query_sequence,
|
|
182
|
+
"query_qualities": list(read.query_qualities)
|
|
183
|
+
if read.query_qualities is not None
|
|
184
|
+
else None,
|
|
185
|
+
"mapping_quality": read.mapping_quality,
|
|
186
|
+
"reference_name": read.reference_name if not read.is_unmapped else None,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def _read_to_element(self, idx: int, read_info: dict) -> Element:
|
|
190
|
+
"""Convert read info to Element with one-hot encoded sequence.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
idx: Index of the read
|
|
194
|
+
read_info: Read metadata dictionary
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Element with sequence, quality scores, and metadata
|
|
198
|
+
"""
|
|
199
|
+
# Encode sequence as one-hot
|
|
200
|
+
sequence_str = read_info["query_sequence"] or ""
|
|
201
|
+
if sequence_str:
|
|
202
|
+
sequence = encode_dna_string(sequence_str, handle_n=self.config.handle_n)
|
|
203
|
+
else:
|
|
204
|
+
sequence = jnp.zeros((0, 4), dtype=jnp.float32)
|
|
205
|
+
|
|
206
|
+
# Get quality scores
|
|
207
|
+
if read_info["query_qualities"] is not None:
|
|
208
|
+
quality_scores = jnp.array(read_info["query_qualities"], dtype=jnp.float32)
|
|
209
|
+
else:
|
|
210
|
+
# Default quality if not available
|
|
211
|
+
quality_scores = jnp.ones(len(sequence_str), dtype=jnp.float32) * 30.0
|
|
212
|
+
|
|
213
|
+
data = {
|
|
214
|
+
"sequence": sequence,
|
|
215
|
+
"quality_scores": quality_scores,
|
|
216
|
+
"read_name": read_info["query_name"],
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
metadata = {
|
|
220
|
+
"idx": idx,
|
|
221
|
+
"reference_name": read_info["reference_name"],
|
|
222
|
+
"reference_start": read_info["reference_start"],
|
|
223
|
+
"mapping_quality": read_info["mapping_quality"],
|
|
224
|
+
"unmapped": read_info["is_unmapped"],
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return Element(data=data, state={}, metadata=metadata) # pyright: ignore[reportArgumentType]
|
|
228
|
+
|
|
229
|
+
def __len__(self) -> int:
|
|
230
|
+
"""Return the number of reads in the source."""
|
|
231
|
+
return len(self._reads)
|
|
232
|
+
|
|
233
|
+
def __getitem__(self, idx: int) -> Element | None:
|
|
234
|
+
"""Get read by index.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
idx: Index of the read
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Element at the given index, or None if out of bounds
|
|
241
|
+
"""
|
|
242
|
+
if idx < 0 or idx >= len(self._reads):
|
|
243
|
+
return None
|
|
244
|
+
return self._read_to_element(idx, self._reads[idx])
|
|
245
|
+
|
|
246
|
+
def __iter__(self) -> Iterator[Element]: # type: ignore[override]
|
|
247
|
+
"""Return iterator over reads."""
|
|
248
|
+
self._current_idx = 0
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def __next__(self) -> Element:
|
|
252
|
+
"""Get next read in iteration."""
|
|
253
|
+
if self._current_idx >= len(self._reads):
|
|
254
|
+
raise StopIteration
|
|
255
|
+
elem = self._read_to_element(self._current_idx, self._reads[self._current_idx])
|
|
256
|
+
self._current_idx += 1
|
|
257
|
+
return elem
|
|
258
|
+
|
|
259
|
+
def _batch_total_size(self) -> int:
|
|
260
|
+
"""Return number of indexed reads for mixin batch iteration."""
|
|
261
|
+
return len(self._reads)
|
|
262
|
+
|
|
263
|
+
def _batch_element(self, idx: int) -> Element:
|
|
264
|
+
"""Build the indexed read element for mixin batch iteration."""
|
|
265
|
+
return self._read_to_element(idx, self._reads[idx])
|