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,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"]
@@ -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))