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,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)
|