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,306 @@
1
+ """BenGRN ground truth DataSource for GRN inference benchmarking.
2
+
3
+ Loads mESC expression data and ChIP+Perturb ground truth edges from
4
+ the benGRN repository (Stone & Sroy gold standards).
5
+
6
+ Data source: /media/mahdi/ssd23/Works/benGRN/data/GroundTruth/
7
+
8
+ References:
9
+ - benGRN: https://github.com/your-org/benGRN
10
+ - Stone & Sroy gold standards
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Iterator
16
+
17
+ import logging
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import jax.numpy as jnp
23
+ import numpy as np
24
+ from flax import nnx
25
+
26
+ from datarax.core.config import StructuralConfig
27
+ from datarax.core.data_source import DataSourceModule
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ _BASE_DIR = Path("/media/mahdi/ssd23/Works/benGRN/data/GroundTruth/stone_and_sroy")
32
+
33
+
34
+ @dataclass(frozen=True, kw_only=True)
35
+ class BenGRNConfig(StructuralConfig):
36
+ """Configuration for BenGRNSource.
37
+
38
+ Attributes:
39
+ data_dir: Root directory of benGRN ground truth data.
40
+ species: Species to load ('mouse' or 'human').
41
+ expression_dataset: Which expression dataset to use.
42
+ ground_truth: Which ground truth network to use.
43
+ max_genes: Maximum number of genes to include (for speed).
44
+ """
45
+
46
+ data_dir: str = str(_BASE_DIR)
47
+ species: str = "mouse"
48
+ expression_dataset: str = "duren"
49
+ ground_truth: str = "chipunion_KDUnion_intersect"
50
+ max_genes: int | None = None
51
+
52
+ def __post_init__(self) -> None:
53
+ """Validate configuration."""
54
+ super().__post_init__()
55
+ base = Path(self.data_dir)
56
+ if not base.exists():
57
+ raise FileNotFoundError(
58
+ f"benGRN data not found: {base}. Clone benGRN repo to ../benGRN/"
59
+ )
60
+
61
+
62
+ class BenGRNSource(DataSourceModule):
63
+ """DataSource for benGRN ground truth GRN data.
64
+
65
+ Loads expression matrix and ground truth regulatory edges for
66
+ GRN inference benchmarking.
67
+ """
68
+
69
+ data: dict[str, Any] = nnx.data()
70
+
71
+ def __init__(
72
+ self,
73
+ config: BenGRNConfig,
74
+ *,
75
+ rngs: nnx.Rngs | None = None,
76
+ name: str | None = None,
77
+ ) -> None:
78
+ """Load the benGRN ground truth data."""
79
+ super().__init__(config, rngs=rngs, name=name or "BenGRNSource")
80
+ self.data = self._load(config)
81
+ logger.info(
82
+ "Loaded benGRN: %d cells, %d genes, %d TFs, %d GT edges",
83
+ self.data["n_cells"],
84
+ self.data["n_genes"],
85
+ self.data["n_tfs"],
86
+ self.data["n_edges"],
87
+ )
88
+
89
+ def _load(self, config: BenGRNConfig) -> dict[str, Any]:
90
+ """Load expression data and ground truth edges.
91
+
92
+ Coordinates loading of expression, TF, and ground truth data,
93
+ then builds the final indexed dataset.
94
+ """
95
+ base = Path(config.data_dir)
96
+ prefix = "mESC" if config.species == "mouse" else "hESC"
97
+
98
+ gene_names, expression = self._load_expression(base, config.expression_dataset)
99
+ n_cells = expression.shape[0]
100
+
101
+ tf_names = self._load_tf_names(base, prefix, config.expression_dataset, gene_names)
102
+ gt_edges = self._load_ground_truth_edges(base, prefix, config.ground_truth)
103
+
104
+ common_genes = self._compute_common_genes(gene_names, gt_edges, config.max_genes)
105
+ gene_to_idx = {g: i for i, g in enumerate(common_genes)}
106
+
107
+ counts = self._build_expression_submatrix(expression, gene_names, common_genes)
108
+ gt_matrix, n_edges = self._build_adjacency_matrix(gt_edges, gene_to_idx)
109
+
110
+ tf_indices = np.array(
111
+ [gene_to_idx[tf] for tf in tf_names if tf in gene_to_idx],
112
+ dtype=np.int32,
113
+ )
114
+
115
+ return {
116
+ "counts": counts,
117
+ "gene_names": common_genes,
118
+ "tf_names": [tf for tf in tf_names if tf in gene_to_idx],
119
+ "tf_indices": tf_indices,
120
+ "ground_truth_matrix": gt_matrix,
121
+ "ground_truth_edges": gt_edges,
122
+ "n_cells": n_cells,
123
+ "n_genes": len(common_genes),
124
+ "n_tfs": len(tf_indices),
125
+ "n_edges": n_edges,
126
+ }
127
+
128
+ def _load_expression(self, base: Path, expression_dataset: str) -> tuple[list[str], np.ndarray]:
129
+ """Load and parse the gzipped expression TSV file.
130
+
131
+ Args:
132
+ base: Root data directory.
133
+ expression_dataset: Name of the expression dataset.
134
+
135
+ Returns:
136
+ Tuple of (gene_names, expression_matrix) where expression_matrix
137
+ is shape (n_cells, n_genes).
138
+
139
+ Raises:
140
+ FileNotFoundError: If the expression file does not exist.
141
+ """
142
+ import gzip # noqa: PLC0415
143
+
144
+ expr_file = base / "scRNA" / f"{expression_dataset}_rna_filtered_log2.tsv.gz"
145
+ if not expr_file.exists():
146
+ raise FileNotFoundError(f"Expression data not found: {expr_file}")
147
+
148
+ with gzip.open(expr_file, "rt") as f:
149
+ lines = f.readlines()
150
+
151
+ # TSV layout: row 0 = cell/sample header, rows 1+ = gene_name \t values...
152
+ gene_names: list[str] = []
153
+ expr_cols: list[list[float]] = []
154
+ for line in lines[1:]:
155
+ parts = line.strip().split("\t")
156
+ gene_names.append(parts[0])
157
+ expr_cols.append([float(v) for v in parts[1:]])
158
+
159
+ # Transpose from (n_genes, n_cells) to (n_cells, n_genes)
160
+ expression = np.array(expr_cols, dtype=np.float32).T
161
+ return gene_names, expression
162
+
163
+ def _load_tf_names(
164
+ self,
165
+ base: Path,
166
+ prefix: str,
167
+ expression_dataset: str,
168
+ gene_names: list[str],
169
+ ) -> list[str]:
170
+ """Load transcription factor names from TSV file.
171
+
172
+ Falls back to the first 50 genes if no TF file is found.
173
+
174
+ Args:
175
+ base: Root data directory.
176
+ prefix: Species prefix (mESC or hESC).
177
+ expression_dataset: Name of the expression dataset.
178
+ gene_names: Gene names from expression data (fallback source).
179
+
180
+ Returns:
181
+ List of transcription factor names.
182
+ """
183
+ tf_file = base / f"{prefix}_{expression_dataset.capitalize()}_TFs.tsv"
184
+ if not tf_file.exists():
185
+ tf_file = base / f"{prefix}_Duren_TFs.tsv"
186
+
187
+ if tf_file.exists():
188
+ return [line.strip() for line in tf_file.read_text().splitlines() if line.strip()]
189
+
190
+ logger.warning("TF file not found, using first 50 genes")
191
+ return gene_names[:50]
192
+
193
+ def _load_ground_truth_edges(
194
+ self, base: Path, prefix: str, ground_truth: str
195
+ ) -> list[tuple[str, str]]:
196
+ """Load ground truth regulatory edges from file.
197
+
198
+ Args:
199
+ base: Root data directory.
200
+ prefix: Species prefix (mESC or hESC).
201
+ ground_truth: Name of the ground truth network.
202
+
203
+ Returns:
204
+ List of (source_gene, target_gene) edge tuples.
205
+
206
+ Raises:
207
+ FileNotFoundError: If the ground truth file does not exist.
208
+ """
209
+ gt_file = base / "gold_standards" / prefix / f"{prefix}_{ground_truth}.txt"
210
+ if not gt_file.exists():
211
+ raise FileNotFoundError(f"Ground truth not found: {gt_file}")
212
+
213
+ gt_edges: list[tuple[str, str]] = []
214
+ for line in gt_file.read_text().splitlines():
215
+ parts = line.strip().split("\t")
216
+ if len(parts) >= 2:
217
+ gt_edges.append((parts[0], parts[1]))
218
+ return gt_edges
219
+
220
+ def _compute_common_genes(
221
+ self,
222
+ gene_names: list[str],
223
+ gt_edges: list[tuple[str, str]],
224
+ max_genes: int | None,
225
+ ) -> list[str]:
226
+ """Compute sorted intersection of expression and ground truth genes.
227
+
228
+ Args:
229
+ gene_names: Gene names from expression data.
230
+ gt_edges: Ground truth edge list.
231
+ max_genes: Optional cap on the number of genes.
232
+
233
+ Returns:
234
+ Sorted list of common gene names.
235
+ """
236
+ expr_gene_set = set(gene_names)
237
+ gt_genes: set[str] = set()
238
+ for src, tgt in gt_edges:
239
+ gt_genes.add(src)
240
+ gt_genes.add(tgt)
241
+
242
+ common_genes = sorted(expr_gene_set & gt_genes)
243
+ if max_genes is not None:
244
+ common_genes = common_genes[:max_genes]
245
+ return common_genes
246
+
247
+ def _build_expression_submatrix(
248
+ self,
249
+ expression: np.ndarray,
250
+ gene_names: list[str],
251
+ common_genes: list[str],
252
+ ) -> jnp.ndarray:
253
+ """Extract expression columns for the common gene set.
254
+
255
+ Args:
256
+ expression: Full expression matrix (n_cells, n_genes).
257
+ gene_names: All gene names corresponding to expression columns.
258
+ common_genes: Subset of genes to keep.
259
+
260
+ Returns:
261
+ JAX array of shape (n_cells, len(common_genes)).
262
+ """
263
+ expr_col_idx = [gene_names.index(g) for g in common_genes if g in gene_names]
264
+ return jnp.array(expression[:, expr_col_idx])
265
+
266
+ def _build_adjacency_matrix(
267
+ self,
268
+ gt_edges: list[tuple[str, str]],
269
+ gene_to_idx: dict[str, int],
270
+ ) -> tuple[np.ndarray, int]:
271
+ """Build binary ground truth adjacency matrix.
272
+
273
+ Args:
274
+ gt_edges: Ground truth edge list.
275
+ gene_to_idx: Mapping from gene name to matrix index.
276
+
277
+ Returns:
278
+ Tuple of (adjacency_matrix, edge_count).
279
+ """
280
+ n_genes = len(gene_to_idx)
281
+ gt_matrix = np.zeros((n_genes, n_genes), dtype=np.float32)
282
+ n_edges = 0
283
+ for src, tgt in gt_edges:
284
+ if src in gene_to_idx and tgt in gene_to_idx:
285
+ gt_matrix[gene_to_idx[src], gene_to_idx[tgt]] = 1.0
286
+ n_edges += 1
287
+ return gt_matrix, n_edges
288
+
289
+ def load(self) -> dict[str, Any]:
290
+ """Return the full dataset."""
291
+ return self.data
292
+
293
+ def __len__(self) -> int:
294
+ """Return number of cells."""
295
+ return self.data["n_cells"]
296
+
297
+ def __iter__(self) -> Iterator[dict[str, Any]]:
298
+ """Iterate over cells."""
299
+ for i in range(len(self)):
300
+ yield {
301
+ k: v[i]
302
+ if hasattr(v, "__getitem__")
303
+ and k not in ("gene_names", "tf_names", "ground_truth_edges")
304
+ else v
305
+ for k, v in self.data.items()
306
+ }
@@ -0,0 +1,242 @@
1
+ """Shared source validation for contextual epigenomics workloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal
6
+
7
+ import jax.numpy as jnp
8
+ import numpy as np
9
+
10
+ from diffbio.sequences.dna import encode_dna_string
11
+
12
+ CONTEXTUAL_EPIGENOMICS_DATASET_CONTRACT_KEYS = (
13
+ "sequence",
14
+ "tf_context",
15
+ "chromatin_contacts",
16
+ "targets",
17
+ )
18
+ CONTEXTUAL_TARGET_SEMANTICS = (
19
+ "binary_peak_mask",
20
+ "chromatin_state_id",
21
+ )
22
+
23
+ _GENERIC_MOTIF = "ACGTAC"
24
+
25
+
26
+ def validate_contextual_epigenomics_dataset(
27
+ data: dict[str, Any],
28
+ *,
29
+ target_semantics: Literal["binary_peak_mask", "chromatin_state_id"] | None = None,
30
+ num_output_classes: int | None = None,
31
+ ) -> None:
32
+ """Validate the shared contextual epigenomics benchmark contract."""
33
+ missing_keys = [key for key in CONTEXTUAL_EPIGENOMICS_DATASET_CONTRACT_KEYS if key not in data]
34
+ if missing_keys:
35
+ raise ValueError(f"Contextual epigenomics data is missing required keys: {missing_keys}")
36
+
37
+ sequence = jnp.asarray(data["sequence"], dtype=jnp.float32)
38
+ tf_context = jnp.asarray(data["tf_context"], dtype=jnp.float32)
39
+ chromatin_contacts = jnp.asarray(data["chromatin_contacts"], dtype=jnp.float32)
40
+ targets = np.asarray(data["targets"])
41
+
42
+ if sequence.ndim != 3 or sequence.shape[-1] != 4:
43
+ raise ValueError("sequence must have shape (n_examples, sequence_length, 4).")
44
+ if tf_context.ndim != 2:
45
+ raise ValueError("tf_context must have shape (n_examples, n_tf_features).")
46
+ if chromatin_contacts.ndim != 3:
47
+ raise ValueError(
48
+ "chromatin_contacts must have shape (n_examples, sequence_length, sequence_length)."
49
+ )
50
+ if targets.ndim != 2:
51
+ raise ValueError("targets must have shape (n_examples, sequence_length).")
52
+
53
+ n_examples = int(sequence.shape[0])
54
+ sequence_length = int(sequence.shape[1])
55
+ if (
56
+ tf_context.shape[0] != n_examples
57
+ or chromatin_contacts.shape[0] != n_examples
58
+ or targets.shape[0] != n_examples
59
+ ):
60
+ raise ValueError(
61
+ "Contextual epigenomics data keys must all share the same leading dimension."
62
+ )
63
+
64
+ if (
65
+ chromatin_contacts.shape[1] != sequence_length
66
+ or chromatin_contacts.shape[2] != sequence_length
67
+ ):
68
+ raise ValueError(
69
+ "chromatin_contacts must align with sequence length and provide a square contact map."
70
+ )
71
+ if targets.shape[1] != sequence_length:
72
+ raise ValueError("targets must have shape (n_examples, sequence_length).")
73
+
74
+ sequence_mass = np.asarray(sequence.sum(axis=-1))
75
+ if not np.allclose(sequence_mass, 1.0, atol=1e-5):
76
+ raise ValueError(
77
+ "sequence must be one-hot or probability-normalized along the alphabet axis."
78
+ )
79
+
80
+ contacts = np.asarray(chromatin_contacts)
81
+ if not np.allclose(contacts, np.swapaxes(contacts, -1, -2), atol=1e-5):
82
+ raise ValueError("chromatin_contacts must be symmetric.")
83
+
84
+ if target_semantics is None:
85
+ return
86
+ if target_semantics not in CONTEXTUAL_TARGET_SEMANTICS:
87
+ raise ValueError(
88
+ "target_semantics must be one of "
89
+ f"{CONTEXTUAL_TARGET_SEMANTICS}, got {target_semantics!r}."
90
+ )
91
+
92
+ _validate_target_semantics(
93
+ targets=targets,
94
+ target_semantics=target_semantics,
95
+ num_output_classes=num_output_classes,
96
+ )
97
+
98
+
99
+ def _validate_target_semantics(
100
+ *,
101
+ targets: np.ndarray,
102
+ target_semantics: Literal["binary_peak_mask", "chromatin_state_id"],
103
+ num_output_classes: int | None,
104
+ ) -> None:
105
+ """Validate target values against the declared contextual task semantics."""
106
+ if target_semantics == "binary_peak_mask":
107
+ unique_targets = set(np.unique(targets).tolist())
108
+ if not unique_targets.issubset({0, 1}):
109
+ raise ValueError("binary_peak_mask targets must contain only 0/1 labels.")
110
+ return
111
+
112
+ if num_output_classes is None or num_output_classes < 1:
113
+ raise ValueError("num_output_classes must be positive for chromatin_state_id targets.")
114
+
115
+ if not np.all(np.equal(targets, np.floor(targets))):
116
+ raise ValueError("chromatin_state_id targets must be integer class IDs.")
117
+
118
+ if np.any(targets < 0) or np.any(targets >= num_output_classes):
119
+ raise ValueError(
120
+ f"chromatin_state_id targets must be in the range [0, {num_output_classes})."
121
+ )
122
+
123
+
124
+ def build_synthetic_contextual_epigenomics_dataset(
125
+ *,
126
+ n_examples: int,
127
+ sequence_length: int,
128
+ num_tf_features: int,
129
+ target_semantics: Literal["binary_peak_mask", "chromatin_state_id"],
130
+ num_output_classes: int,
131
+ ) -> dict[str, jnp.ndarray]:
132
+ """Build a deterministic contextual epigenomics dataset."""
133
+ if target_semantics not in CONTEXTUAL_TARGET_SEMANTICS:
134
+ raise ValueError(
135
+ "target_semantics must be one of "
136
+ f"{CONTEXTUAL_TARGET_SEMANTICS}, got {target_semantics!r}."
137
+ )
138
+
139
+ sequences: list[str] = []
140
+ tf_context_rows: list[np.ndarray] = []
141
+ chromatin_contacts: list[np.ndarray] = []
142
+ targets: list[np.ndarray] = []
143
+
144
+ motif_length = len(_GENERIC_MOTIF)
145
+ peak_width = max(motif_length, 4)
146
+ candidate_starts = _build_candidate_starts(
147
+ sequence_length=sequence_length,
148
+ num_tf_features=num_tf_features,
149
+ peak_width=peak_width,
150
+ )
151
+ template_sequence = _build_sequence_template(
152
+ sequence_length=sequence_length,
153
+ candidate_starts=candidate_starts,
154
+ motif=_GENERIC_MOTIF,
155
+ )
156
+
157
+ for example_index in range(n_examples):
158
+ tf_index = example_index % num_tf_features
159
+ region_start = candidate_starts[tf_index]
160
+ region_end = min(region_start + peak_width, sequence_length)
161
+ sequences.append(template_sequence)
162
+
163
+ tf_row = np.zeros(num_tf_features, dtype=np.float32)
164
+ tf_row[tf_index] = 1.0
165
+ tf_context_rows.append(tf_row)
166
+
167
+ contact_map = np.eye(sequence_length, dtype=np.float32) * 0.1
168
+ contact_map[region_start:region_end, region_start:region_end] = 1.0
169
+ flank_start = max(region_start - 1, 0)
170
+ flank_end = min(region_end + 1, sequence_length)
171
+ contact_map[flank_start:flank_end, flank_start:flank_end] = np.maximum(
172
+ contact_map[flank_start:flank_end, flank_start:flank_end],
173
+ 0.5,
174
+ )
175
+ contact_map = np.maximum(contact_map, contact_map.T)
176
+ chromatin_contacts.append(contact_map)
177
+
178
+ target = np.zeros(sequence_length, dtype=np.int32)
179
+ if target_semantics == "binary_peak_mask":
180
+ target[region_start:region_end] = 1
181
+ else:
182
+ target[region_start:region_end] = 1
183
+ right_flank_end = min(region_end + peak_width // 2, sequence_length)
184
+ target[region_end:right_flank_end] = 2 % num_output_classes
185
+ targets.append(target)
186
+
187
+ one_hot_sequences = jnp.asarray(
188
+ np.stack(
189
+ [np.asarray(encode_dna_string(sequence), dtype=np.float32) for sequence in sequences]
190
+ ),
191
+ dtype=jnp.float32,
192
+ )
193
+ dataset = {
194
+ "sequence": one_hot_sequences,
195
+ "tf_context": jnp.asarray(np.stack(tf_context_rows), dtype=jnp.float32),
196
+ "chromatin_contacts": jnp.asarray(np.stack(chromatin_contacts), dtype=jnp.float32),
197
+ "targets": jnp.asarray(np.stack(targets)),
198
+ }
199
+ validate_contextual_epigenomics_dataset(
200
+ dataset,
201
+ target_semantics=target_semantics,
202
+ num_output_classes=num_output_classes,
203
+ )
204
+ return dataset
205
+
206
+
207
+ def _build_candidate_starts(
208
+ *,
209
+ sequence_length: int,
210
+ num_tf_features: int,
211
+ peak_width: int,
212
+ ) -> list[int]:
213
+ """Build deterministic candidate windows selected by TF context."""
214
+ if sequence_length < peak_width + 2:
215
+ return [0 for _ in range(num_tf_features)]
216
+
217
+ max_start = max(sequence_length - peak_width - 1, 1)
218
+ raw_starts = np.linspace(
219
+ 1,
220
+ max_start,
221
+ num=num_tf_features,
222
+ dtype=np.int32,
223
+ )
224
+ return [int(start) for start in raw_starts]
225
+
226
+
227
+ def _build_sequence_template(
228
+ *,
229
+ sequence_length: int,
230
+ candidate_starts: list[int],
231
+ motif: str,
232
+ ) -> str:
233
+ """Build one shared sequence carrying several candidate regulatory windows."""
234
+ sequence = ["A"] * sequence_length
235
+ motif_chars = list(motif)
236
+ motif_length = len(motif_chars)
237
+
238
+ for start in candidate_starts:
239
+ end = min(start + motif_length, sequence_length)
240
+ sequence[start:end] = motif_chars[: end - start]
241
+
242
+ return "".join(sequence)