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.
Files changed (202) hide show
  1. diffbio/__init__.py +39 -0
  2. diffbio/configs.py +75 -0
  3. diffbio/constants.py +204 -0
  4. diffbio/core/__init__.py +127 -0
  5. diffbio/core/base_operators.py +612 -0
  6. diffbio/core/data_types.py +260 -0
  7. diffbio/core/gnn_components.py +629 -0
  8. diffbio/core/graph_utils.py +149 -0
  9. diffbio/core/neural_components.py +270 -0
  10. diffbio/core/optimal_transport.py +133 -0
  11. diffbio/core/soft_ops/__init__.py +216 -0
  12. diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
  13. diffbio/core/soft_ops/_projections_simplex.py +240 -0
  14. diffbio/core/soft_ops/_projections_transport.py +508 -0
  15. diffbio/core/soft_ops/_sorting_network.py +204 -0
  16. diffbio/core/soft_ops/_types.py +15 -0
  17. diffbio/core/soft_ops/_utils.py +342 -0
  18. diffbio/core/soft_ops/autograd_safe.py +120 -0
  19. diffbio/core/soft_ops/comparison.py +235 -0
  20. diffbio/core/soft_ops/elementwise.py +309 -0
  21. diffbio/core/soft_ops/logical.py +146 -0
  22. diffbio/core/soft_ops/quantile.py +376 -0
  23. diffbio/core/soft_ops/selection.py +236 -0
  24. diffbio/core/soft_ops/sorting.py +926 -0
  25. diffbio/core/soft_ops/straight_through.py +261 -0
  26. diffbio/core/uncertainty.py +279 -0
  27. diffbio/evaluation/__init__.py +42 -0
  28. diffbio/evaluation/adapters.py +409 -0
  29. diffbio/evaluation/graders.py +223 -0
  30. diffbio/evaluation/problem.py +157 -0
  31. diffbio/evaluation/runner.py +277 -0
  32. diffbio/losses/__init__.py +59 -0
  33. diffbio/losses/alignment_losses.py +222 -0
  34. diffbio/losses/biological_regularization.py +288 -0
  35. diffbio/losses/metric_losses.py +139 -0
  36. diffbio/losses/singlecell_losses.py +387 -0
  37. diffbio/losses/statistical_losses.py +345 -0
  38. diffbio/operators/__init__.py +60 -0
  39. diffbio/operators/_count_vae.py +197 -0
  40. diffbio/operators/_loss_balancing.py +65 -0
  41. diffbio/operators/_masked_gene_transformer.py +118 -0
  42. diffbio/operators/_transformer_validation.py +50 -0
  43. diffbio/operators/alignment/__init__.py +51 -0
  44. diffbio/operators/alignment/profile_hmm.py +350 -0
  45. diffbio/operators/alignment/scoring.py +127 -0
  46. diffbio/operators/alignment/smith_waterman.py +261 -0
  47. diffbio/operators/alignment/soft_msa.py +419 -0
  48. diffbio/operators/assembly/__init__.py +27 -0
  49. diffbio/operators/assembly/gnn_assembly.py +252 -0
  50. diffbio/operators/assembly/metagenomic_binning.py +296 -0
  51. diffbio/operators/crispr/__init__.py +17 -0
  52. diffbio/operators/crispr/guide_scoring.py +269 -0
  53. diffbio/operators/drug_discovery/__init__.py +133 -0
  54. diffbio/operators/drug_discovery/_graph_utils.py +142 -0
  55. diffbio/operators/drug_discovery/admet_predictor.py +285 -0
  56. diffbio/operators/drug_discovery/attentive_fp.py +411 -0
  57. diffbio/operators/drug_discovery/dti.py +261 -0
  58. diffbio/operators/drug_discovery/fingerprint.py +490 -0
  59. diffbio/operators/drug_discovery/maccs_keys.py +267 -0
  60. diffbio/operators/drug_discovery/message_passing.py +200 -0
  61. diffbio/operators/drug_discovery/primitives.py +242 -0
  62. diffbio/operators/drug_discovery/property_predictor.py +163 -0
  63. diffbio/operators/drug_discovery/similarity.py +193 -0
  64. diffbio/operators/epigenomics/__init__.py +35 -0
  65. diffbio/operators/epigenomics/chromatin_state.py +491 -0
  66. diffbio/operators/epigenomics/contextual.py +288 -0
  67. diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
  68. diffbio/operators/epigenomics/peak_calling.py +555 -0
  69. diffbio/operators/foundation_models/__init__.py +119 -0
  70. diffbio/operators/foundation_models/adapters.py +114 -0
  71. diffbio/operators/foundation_models/contracts.py +245 -0
  72. diffbio/operators/foundation_models/embedding_probe.py +83 -0
  73. diffbio/operators/foundation_models/experimental.py +128 -0
  74. diffbio/operators/foundation_models/foundation_model.py +332 -0
  75. diffbio/operators/foundation_models/frozen.py +59 -0
  76. diffbio/operators/foundation_models/precomputed.py +270 -0
  77. diffbio/operators/foundation_models/transformer_encoder.py +564 -0
  78. diffbio/operators/mapping/__init__.py +17 -0
  79. diffbio/operators/mapping/neural_mapper.py +493 -0
  80. diffbio/operators/metabolomics/__init__.py +39 -0
  81. diffbio/operators/metabolomics/spectral_similarity.py +315 -0
  82. diffbio/operators/molecular_dynamics/__init__.py +51 -0
  83. diffbio/operators/molecular_dynamics/force_field.py +265 -0
  84. diffbio/operators/molecular_dynamics/integrator.py +304 -0
  85. diffbio/operators/molecular_dynamics/primitives.py +115 -0
  86. diffbio/operators/multiomics/__init__.py +38 -0
  87. diffbio/operators/multiomics/hic_contact.py +377 -0
  88. diffbio/operators/multiomics/multiomics_vae.py +325 -0
  89. diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
  90. diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
  91. diffbio/operators/normalization/__init__.py +42 -0
  92. diffbio/operators/normalization/embedding.py +222 -0
  93. diffbio/operators/normalization/phate.py +400 -0
  94. diffbio/operators/normalization/umap.py +261 -0
  95. diffbio/operators/normalization/vae_normalizer.py +258 -0
  96. diffbio/operators/population/__init__.py +17 -0
  97. diffbio/operators/population/ancestry_estimation.py +274 -0
  98. diffbio/operators/preprocessing/__init__.py +76 -0
  99. diffbio/operators/preprocessing/adapter_removal.py +311 -0
  100. diffbio/operators/preprocessing/duplicate_filter.py +317 -0
  101. diffbio/operators/preprocessing/error_correction.py +287 -0
  102. diffbio/operators/protein/__init__.py +31 -0
  103. diffbio/operators/protein/secondary_structure.py +509 -0
  104. diffbio/operators/quality_filter.py +128 -0
  105. diffbio/operators/rna_structure/__init__.py +35 -0
  106. diffbio/operators/rna_structure/rna_folding.py +509 -0
  107. diffbio/operators/rnaseq/__init__.py +23 -0
  108. diffbio/operators/rnaseq/motif_discovery.py +251 -0
  109. diffbio/operators/rnaseq/splicing_psi.py +216 -0
  110. diffbio/operators/singlecell/__init__.py +193 -0
  111. diffbio/operators/singlecell/ambient_removal.py +333 -0
  112. diffbio/operators/singlecell/archetypes.py +191 -0
  113. diffbio/operators/singlecell/batch_correction.py +288 -0
  114. diffbio/operators/singlecell/cell_annotation.py +519 -0
  115. diffbio/operators/singlecell/communication.py +704 -0
  116. diffbio/operators/singlecell/differential_distribution.py +243 -0
  117. diffbio/operators/singlecell/doublet_detection.py +657 -0
  118. diffbio/operators/singlecell/downsampling.py +166 -0
  119. diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
  120. diffbio/operators/singlecell/grn_inference.py +336 -0
  121. diffbio/operators/singlecell/imputation.py +429 -0
  122. diffbio/operators/singlecell/knockdown_filter.py +176 -0
  123. diffbio/operators/singlecell/ot_trajectory.py +277 -0
  124. diffbio/operators/singlecell/simulation.py +444 -0
  125. diffbio/operators/singlecell/sindy_grn.py +247 -0
  126. diffbio/operators/singlecell/soft_clustering.py +211 -0
  127. diffbio/operators/singlecell/spatial_domains.py +677 -0
  128. diffbio/operators/singlecell/switch_de.py +184 -0
  129. diffbio/operators/singlecell/trajectory.py +447 -0
  130. diffbio/operators/singlecell/velocity.py +361 -0
  131. diffbio/operators/statistical/__init__.py +35 -0
  132. diffbio/operators/statistical/em_quantification.py +260 -0
  133. diffbio/operators/statistical/hmm.py +234 -0
  134. diffbio/operators/statistical/nb_glm.py +272 -0
  135. diffbio/operators/variant/__init__.py +64 -0
  136. diffbio/operators/variant/classifier.py +333 -0
  137. diffbio/operators/variant/cnn_classifier.py +255 -0
  138. diffbio/operators/variant/cnv_segmentation.py +678 -0
  139. diffbio/operators/variant/deepvariant_pileup.py +426 -0
  140. diffbio/operators/variant/pileup.py +240 -0
  141. diffbio/operators/variant/quality_recalibration.py +274 -0
  142. diffbio/pipelines/__init__.py +65 -0
  143. diffbio/pipelines/differential_expression.py +279 -0
  144. diffbio/pipelines/enhanced_variant_calling.py +326 -0
  145. diffbio/pipelines/perturbation.py +407 -0
  146. diffbio/pipelines/preprocessing.py +267 -0
  147. diffbio/pipelines/single_cell.py +366 -0
  148. diffbio/pipelines/variant_calling.py +490 -0
  149. diffbio/samplers/__init__.py +9 -0
  150. diffbio/samplers/perturbation_sampler.py +142 -0
  151. diffbio/sequences/__init__.py +34 -0
  152. diffbio/sequences/dna.py +239 -0
  153. diffbio/sources/__init__.py +149 -0
  154. diffbio/sources/_anndata_shared.py +89 -0
  155. diffbio/sources/_batch_iteration.py +37 -0
  156. diffbio/sources/_benchmark_source.py +152 -0
  157. diffbio/sources/_indexed_batch_source.py +38 -0
  158. diffbio/sources/_utils.py +45 -0
  159. diffbio/sources/anndata_interop.py +387 -0
  160. diffbio/sources/anndata_source.py +361 -0
  161. diffbio/sources/archive_ii.py +174 -0
  162. diffbio/sources/balifam.py +207 -0
  163. diffbio/sources/bam.py +265 -0
  164. diffbio/sources/bengrn_ground_truth.py +306 -0
  165. diffbio/sources/contextual_epigenomics.py +242 -0
  166. diffbio/sources/dti.py +359 -0
  167. diffbio/sources/embeddings.py +203 -0
  168. diffbio/sources/encode_peaks.py +223 -0
  169. diffbio/sources/fasta.py +226 -0
  170. diffbio/sources/immune_human.py +172 -0
  171. diffbio/sources/indexed_embeddings.py +128 -0
  172. diffbio/sources/indexed_view.py +191 -0
  173. diffbio/sources/molnet.py +493 -0
  174. diffbio/sources/multiomics.py +279 -0
  175. diffbio/sources/pancreas.py +108 -0
  176. diffbio/sources/perturbation/__init__.py +69 -0
  177. diffbio/sources/perturbation/_types.py +51 -0
  178. diffbio/sources/perturbation/_utils.py +125 -0
  179. diffbio/sources/perturbation/concat_source.py +115 -0
  180. diffbio/sources/perturbation/control_mapping.py +215 -0
  181. diffbio/sources/perturbation/experiment_config.py +261 -0
  182. diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
  183. diffbio/sources/perturbation/output_space.py +52 -0
  184. diffbio/sources/perturbation/perturbation_source.py +513 -0
  185. diffbio/sources/seqfish.py +145 -0
  186. diffbio/sources/sequence_foundation.py +68 -0
  187. diffbio/sources/singlecell_foundation.py +68 -0
  188. diffbio/splitters/__init__.py +63 -0
  189. diffbio/splitters/base.py +251 -0
  190. diffbio/splitters/molecular.py +330 -0
  191. diffbio/splitters/perturbation.py +199 -0
  192. diffbio/splitters/random.py +217 -0
  193. diffbio/splitters/sequence.py +201 -0
  194. diffbio/utils/__init__.py +55 -0
  195. diffbio/utils/dependency_runtime.py +115 -0
  196. diffbio/utils/nn_utils.py +157 -0
  197. diffbio/utils/quality.py +45 -0
  198. diffbio/utils/training.py +585 -0
  199. diffbio-0.1.0.dist-info/METADATA +480 -0
  200. diffbio-0.1.0.dist-info/RECORD +202 -0
  201. diffbio-0.1.0.dist-info/WHEEL +4 -0
  202. 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])