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,288 @@
|
|
|
1
|
+
"""Contextual epigenomics operator for sequence, TF, and chromatin inputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import jax
|
|
9
|
+
import jax.numpy as jnp
|
|
10
|
+
import optax
|
|
11
|
+
from artifex.generative_models.core.layers import TransformerEncoder
|
|
12
|
+
from datarax.core.config import OperatorConfig
|
|
13
|
+
from datarax.core.operator import OperatorModule
|
|
14
|
+
from flax import nnx
|
|
15
|
+
|
|
16
|
+
from diffbio.operators._transformer_validation import TransformerEncoderShapeValidationMixin
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class _ContextualEncoderConfig:
|
|
21
|
+
"""Sequence encoder hyperparameters."""
|
|
22
|
+
|
|
23
|
+
hidden_dim: int = 64
|
|
24
|
+
num_layers: int = 2
|
|
25
|
+
num_heads: int = 4
|
|
26
|
+
intermediate_dim: int = 256
|
|
27
|
+
max_length: int = 512
|
|
28
|
+
dropout_rate: float = 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class _ContextualTaskConfig:
|
|
33
|
+
"""Task and conditioning configuration."""
|
|
34
|
+
|
|
35
|
+
num_tf_features: int = 8
|
|
36
|
+
num_outputs: int = 1
|
|
37
|
+
use_tf_context: bool = True
|
|
38
|
+
use_chromatin_guidance: bool = False
|
|
39
|
+
chromatin_guidance_weight: float = 0.1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ContextualEpigenomicsConfig(
|
|
44
|
+
_ContextualEncoderConfig,
|
|
45
|
+
_ContextualTaskConfig,
|
|
46
|
+
TransformerEncoderShapeValidationMixin,
|
|
47
|
+
OperatorConfig,
|
|
48
|
+
):
|
|
49
|
+
"""Configuration for the contextual epigenomics operator."""
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
"""Validate the operator configuration."""
|
|
53
|
+
super().__post_init__()
|
|
54
|
+
if self.num_outputs < 1:
|
|
55
|
+
raise ValueError("num_outputs must be at least 1.")
|
|
56
|
+
if self.num_tf_features < 1:
|
|
57
|
+
raise ValueError("num_tf_features must be at least 1.")
|
|
58
|
+
if self.chromatin_guidance_weight < 0.0:
|
|
59
|
+
raise ValueError("chromatin_guidance_weight must be non-negative.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ContextualEpigenomicsOperator(OperatorModule):
|
|
63
|
+
"""Single operator path for sequence-only and contextual epigenomics modes."""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
config: ContextualEpigenomicsConfig,
|
|
68
|
+
*,
|
|
69
|
+
rngs: nnx.Rngs | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
super().__init__(config, rngs=rngs)
|
|
72
|
+
|
|
73
|
+
if rngs is None:
|
|
74
|
+
rngs = nnx.Rngs(0)
|
|
75
|
+
if config.dropout_rate > 0 and "dropout" not in rngs:
|
|
76
|
+
rngs = nnx.Rngs(params=rngs.params(), dropout=jax.random.key(1))
|
|
77
|
+
|
|
78
|
+
self.config = config
|
|
79
|
+
self.sequence_projection = nnx.Linear(4, config.hidden_dim, rngs=rngs)
|
|
80
|
+
self.tf_scale = nnx.Linear(config.num_tf_features, config.hidden_dim, rngs=rngs)
|
|
81
|
+
self.tf_shift = nnx.Linear(config.num_tf_features, config.hidden_dim, rngs=rngs)
|
|
82
|
+
self.transformer = TransformerEncoder(
|
|
83
|
+
num_layers=config.num_layers,
|
|
84
|
+
hidden_dim=config.hidden_dim,
|
|
85
|
+
num_heads=config.num_heads,
|
|
86
|
+
mlp_ratio=config.intermediate_dim / config.hidden_dim,
|
|
87
|
+
dropout_rate=config.dropout_rate,
|
|
88
|
+
attention_dropout_rate=0.0,
|
|
89
|
+
max_len=config.max_length,
|
|
90
|
+
pos_encoding_type="sinusoidal",
|
|
91
|
+
rngs=rngs,
|
|
92
|
+
)
|
|
93
|
+
self.output_head = nnx.Linear(config.hidden_dim, config.num_outputs, rngs=rngs)
|
|
94
|
+
|
|
95
|
+
def apply(
|
|
96
|
+
self,
|
|
97
|
+
data: dict[str, Any],
|
|
98
|
+
state: dict[str, Any],
|
|
99
|
+
metadata: dict[str, Any] | None,
|
|
100
|
+
random_params: Any = None,
|
|
101
|
+
stats: dict[str, Any] | None = None,
|
|
102
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]:
|
|
103
|
+
"""Apply the contextual epigenomics operator to one batch."""
|
|
104
|
+
del random_params, stats
|
|
105
|
+
|
|
106
|
+
sequence = jnp.asarray(data["sequence"], dtype=jnp.float32)
|
|
107
|
+
tf_context = data.get("tf_context")
|
|
108
|
+
chromatin_contacts = data.get("chromatin_contacts")
|
|
109
|
+
sequence_mask = data.get("sequence_mask")
|
|
110
|
+
|
|
111
|
+
(
|
|
112
|
+
sequence,
|
|
113
|
+
tf_context,
|
|
114
|
+
chromatin_contacts,
|
|
115
|
+
sequence_mask,
|
|
116
|
+
squeeze_batch,
|
|
117
|
+
) = _canonicalize_contextual_inputs(
|
|
118
|
+
sequence=sequence,
|
|
119
|
+
tf_context=tf_context,
|
|
120
|
+
chromatin_contacts=chromatin_contacts,
|
|
121
|
+
sequence_mask=sequence_mask,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
hidden = self.sequence_projection(sequence)
|
|
125
|
+
if self.config.use_tf_context:
|
|
126
|
+
if tf_context is None:
|
|
127
|
+
raise ValueError("tf_context is required when use_tf_context=True.")
|
|
128
|
+
hidden = _apply_tf_conditioning(
|
|
129
|
+
hidden=hidden,
|
|
130
|
+
tf_context=jnp.asarray(tf_context, dtype=jnp.float32),
|
|
131
|
+
tf_scale=self.tf_scale,
|
|
132
|
+
tf_shift=self.tf_shift,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
token_embeddings = self.transformer(
|
|
136
|
+
hidden,
|
|
137
|
+
mask=sequence_mask,
|
|
138
|
+
deterministic=True,
|
|
139
|
+
)
|
|
140
|
+
masked_token_embeddings = token_embeddings * sequence_mask[..., None]
|
|
141
|
+
pooled_embeddings = masked_token_embeddings.sum(axis=1) / jnp.maximum(
|
|
142
|
+
sequence_mask.sum(axis=1, keepdims=True),
|
|
143
|
+
1.0,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
logits = self.output_head(token_embeddings)
|
|
147
|
+
if self.config.num_outputs == 1:
|
|
148
|
+
logits = logits.squeeze(-1)
|
|
149
|
+
|
|
150
|
+
chromatin_guidance_loss = jnp.array(0.0, dtype=jnp.float32)
|
|
151
|
+
if self.config.use_chromatin_guidance:
|
|
152
|
+
if chromatin_contacts is None:
|
|
153
|
+
raise ValueError("chromatin_contacts is required when use_chromatin_guidance=True.")
|
|
154
|
+
chromatin_guidance_loss = compute_chromatin_guidance_loss(
|
|
155
|
+
token_embeddings=token_embeddings,
|
|
156
|
+
chromatin_contacts=jnp.asarray(chromatin_contacts, dtype=jnp.float32),
|
|
157
|
+
sequence_mask=sequence_mask,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
result = {
|
|
161
|
+
**data,
|
|
162
|
+
"embeddings": pooled_embeddings,
|
|
163
|
+
"token_embeddings": token_embeddings,
|
|
164
|
+
"logits": logits,
|
|
165
|
+
"chromatin_guidance_loss": chromatin_guidance_loss,
|
|
166
|
+
}
|
|
167
|
+
if squeeze_batch:
|
|
168
|
+
result["embeddings"] = pooled_embeddings.squeeze(0)
|
|
169
|
+
result["token_embeddings"] = token_embeddings.squeeze(0)
|
|
170
|
+
result["logits"] = logits.squeeze(0)
|
|
171
|
+
|
|
172
|
+
return result, state, metadata
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _canonicalize_contextual_inputs(
|
|
176
|
+
*,
|
|
177
|
+
sequence: jnp.ndarray,
|
|
178
|
+
tf_context: Any,
|
|
179
|
+
chromatin_contacts: Any,
|
|
180
|
+
sequence_mask: Any,
|
|
181
|
+
) -> tuple[jnp.ndarray, jnp.ndarray | None, jnp.ndarray | None, jnp.ndarray, bool]:
|
|
182
|
+
"""Normalize contextual epigenomics inputs to batched tensors."""
|
|
183
|
+
if sequence.ndim not in (2, 3) or sequence.shape[-1] != 4:
|
|
184
|
+
raise ValueError("sequence must have shape (length, 4) or (batch, length, 4).")
|
|
185
|
+
|
|
186
|
+
squeeze_batch = sequence.ndim == 2
|
|
187
|
+
if squeeze_batch:
|
|
188
|
+
sequence = sequence[None, ...]
|
|
189
|
+
|
|
190
|
+
batch_size, sequence_length, _ = sequence.shape
|
|
191
|
+
|
|
192
|
+
tf_tensor: jnp.ndarray | None = None
|
|
193
|
+
if tf_context is not None:
|
|
194
|
+
tf_tensor = jnp.asarray(tf_context, dtype=jnp.float32)
|
|
195
|
+
if tf_tensor.ndim == 1:
|
|
196
|
+
tf_tensor = tf_tensor[None, ...]
|
|
197
|
+
if tf_tensor.ndim != 2 or tf_tensor.shape[0] != batch_size:
|
|
198
|
+
raise ValueError("tf_context must have shape (features,) or (batch, features).")
|
|
199
|
+
|
|
200
|
+
chromatin_tensor: jnp.ndarray | None = None
|
|
201
|
+
if chromatin_contacts is not None:
|
|
202
|
+
chromatin_tensor = jnp.asarray(chromatin_contacts, dtype=jnp.float32)
|
|
203
|
+
if chromatin_tensor.ndim == 2:
|
|
204
|
+
chromatin_tensor = chromatin_tensor[None, ...]
|
|
205
|
+
if chromatin_tensor.ndim != 3 or chromatin_tensor.shape[0] != batch_size:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
"chromatin_contacts must have shape (length, length) or (batch, length, length)."
|
|
208
|
+
)
|
|
209
|
+
if (
|
|
210
|
+
chromatin_tensor.shape[1] != sequence_length
|
|
211
|
+
or chromatin_tensor.shape[2] != sequence_length
|
|
212
|
+
):
|
|
213
|
+
raise ValueError("chromatin_contacts must align with the sequence length.")
|
|
214
|
+
|
|
215
|
+
if sequence_mask is None:
|
|
216
|
+
mask_tensor = jnp.ones((batch_size, sequence_length), dtype=jnp.float32)
|
|
217
|
+
else:
|
|
218
|
+
mask_tensor = jnp.asarray(sequence_mask, dtype=jnp.float32)
|
|
219
|
+
if mask_tensor.ndim == 1:
|
|
220
|
+
mask_tensor = mask_tensor[None, ...]
|
|
221
|
+
if mask_tensor.shape != (batch_size, sequence_length):
|
|
222
|
+
raise ValueError("sequence_mask must have shape (length,) or (batch, length).")
|
|
223
|
+
|
|
224
|
+
return sequence, tf_tensor, chromatin_tensor, mask_tensor, squeeze_batch
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _apply_tf_conditioning(
|
|
228
|
+
*,
|
|
229
|
+
hidden: jnp.ndarray,
|
|
230
|
+
tf_context: jnp.ndarray,
|
|
231
|
+
tf_scale: nnx.Linear,
|
|
232
|
+
tf_shift: nnx.Linear,
|
|
233
|
+
) -> jnp.ndarray:
|
|
234
|
+
"""Apply FiLM-style TF conditioning to token embeddings."""
|
|
235
|
+
scale = jnp.tanh(tf_scale(tf_context))[:, None, :]
|
|
236
|
+
shift = tf_shift(tf_context)[:, None, :]
|
|
237
|
+
return hidden * (1.0 + scale) + shift
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def compute_chromatin_guidance_loss(
|
|
241
|
+
*,
|
|
242
|
+
token_embeddings: jnp.ndarray,
|
|
243
|
+
chromatin_contacts: jnp.ndarray,
|
|
244
|
+
sequence_mask: jnp.ndarray,
|
|
245
|
+
) -> jnp.ndarray:
|
|
246
|
+
"""Compute a chromatin-consistency loss over token embeddings."""
|
|
247
|
+
normalized_embeddings = token_embeddings / jnp.maximum(
|
|
248
|
+
jnp.linalg.norm(token_embeddings, axis=-1, keepdims=True),
|
|
249
|
+
1e-6,
|
|
250
|
+
)
|
|
251
|
+
similarity = jnp.einsum(
|
|
252
|
+
"bld,bmd->blm",
|
|
253
|
+
normalized_embeddings,
|
|
254
|
+
normalized_embeddings,
|
|
255
|
+
)
|
|
256
|
+
predicted_contacts = jax.nn.sigmoid(similarity)
|
|
257
|
+
pair_mask = sequence_mask[:, :, None] * sequence_mask[:, None, :]
|
|
258
|
+
squared_error = jnp.square(predicted_contacts - chromatin_contacts) * pair_mask
|
|
259
|
+
return squared_error.sum() / jnp.maximum(pair_mask.sum(), 1.0)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def compute_contextual_epigenomics_loss(
|
|
263
|
+
model: ContextualEpigenomicsOperator,
|
|
264
|
+
data: dict[str, Any],
|
|
265
|
+
) -> dict[str, jnp.ndarray]:
|
|
266
|
+
"""Compute supervised plus optional chromatin-guidance losses."""
|
|
267
|
+
result = model.apply(data, {}, None)[0]
|
|
268
|
+
logits = jnp.asarray(result["logits"], dtype=jnp.float32)
|
|
269
|
+
targets = jnp.asarray(data["targets"])
|
|
270
|
+
|
|
271
|
+
if logits.ndim == targets.ndim:
|
|
272
|
+
supervised = optax.sigmoid_binary_cross_entropy(
|
|
273
|
+
logits,
|
|
274
|
+
targets.astype(jnp.float32),
|
|
275
|
+
).mean()
|
|
276
|
+
else:
|
|
277
|
+
supervised = optax.softmax_cross_entropy_with_integer_labels(
|
|
278
|
+
logits,
|
|
279
|
+
targets.astype(jnp.int32),
|
|
280
|
+
).mean()
|
|
281
|
+
|
|
282
|
+
chromatin_guidance = jnp.asarray(result["chromatin_guidance_loss"], dtype=jnp.float32)
|
|
283
|
+
total = supervised + model.config.chromatin_guidance_weight * chromatin_guidance
|
|
284
|
+
return {
|
|
285
|
+
"supervised": supervised,
|
|
286
|
+
"chromatin_guidance": chromatin_guidance,
|
|
287
|
+
"total": total,
|
|
288
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""FNO-based differentiable peak calling for ChIP-seq and ATAC-seq data.
|
|
2
|
+
|
|
3
|
+
Uses a Fourier Neural Operator (FNO) from opifex to learn the mapping
|
|
4
|
+
from coverage signals to peak probabilities. The FNO captures multi-scale
|
|
5
|
+
patterns in the frequency domain, making it well-suited for detecting
|
|
6
|
+
peaks of varying widths without explicit multi-scale CNN kernels.
|
|
7
|
+
|
|
8
|
+
This is an alternative to the CNN-based ``DifferentiablePeakCaller``
|
|
9
|
+
in ``peak_calling.py``. The FNO approach processes the entire signal
|
|
10
|
+
in one pass via spectral convolutions rather than sliding windows.
|
|
11
|
+
|
|
12
|
+
Reference:
|
|
13
|
+
Li et al. (2021) "Fourier Neural Operator for Parametric Partial
|
|
14
|
+
Differential Equations". ICLR 2021.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import jax.numpy as jnp
|
|
24
|
+
from datarax.core.config import OperatorConfig
|
|
25
|
+
from datarax.core.operator import OperatorModule
|
|
26
|
+
from flax import nnx
|
|
27
|
+
|
|
28
|
+
from diffbio.core import soft_ops
|
|
29
|
+
from jaxtyping import PyTree
|
|
30
|
+
from opifex.neural.operators import FourierNeuralOperator
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class FNOPeakCallerConfig(OperatorConfig):
|
|
37
|
+
"""Configuration for FNO-based peak caller.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
hidden_channels: Number of hidden channels in FNO layers.
|
|
41
|
+
modes: Number of Fourier modes to retain (controls frequency resolution).
|
|
42
|
+
num_layers: Number of FNO layers.
|
|
43
|
+
threshold: Initial soft threshold for peak classification.
|
|
44
|
+
temperature: Temperature for sigmoid smoothing.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
hidden_channels: int = 32
|
|
48
|
+
modes: int = 16
|
|
49
|
+
num_layers: int = 4
|
|
50
|
+
threshold: float = 0.5
|
|
51
|
+
temperature: float = 1.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FNOPeakCaller(OperatorModule):
|
|
55
|
+
"""FNO-based differentiable peak caller.
|
|
56
|
+
|
|
57
|
+
Applies a Fourier Neural Operator to map coverage signals to peak
|
|
58
|
+
probability scores. The FNO learns spectral convolution kernels that
|
|
59
|
+
capture peak patterns at multiple frequency scales simultaneously.
|
|
60
|
+
|
|
61
|
+
Input data:
|
|
62
|
+
- ``"coverage"``: Coverage signal ``(batch, length)`` or ``(length,)``.
|
|
63
|
+
|
|
64
|
+
Output adds:
|
|
65
|
+
- ``"peak_scores"``: Raw FNO output scores ``(batch, length)``.
|
|
66
|
+
- ``"peak_probabilities"``: Sigmoid-transformed scores in [0, 1].
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
config: FNOPeakCallerConfig with FNO parameters.
|
|
70
|
+
rngs: Flax NNX random number generators.
|
|
71
|
+
name: Optional operator name.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> config = FNOPeakCallerConfig(hidden_channels=32, modes=16)
|
|
75
|
+
>>> op = FNOPeakCaller(config, rngs=nnx.Rngs(42))
|
|
76
|
+
>>> result, _, _ = op.apply({"coverage": coverage}, {}, None)
|
|
77
|
+
>>> peaks = result["peak_probabilities"] > 0.5
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
config: FNOPeakCallerConfig,
|
|
83
|
+
*,
|
|
84
|
+
rngs: nnx.Rngs | None = None,
|
|
85
|
+
name: str | None = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Initialize FNO peak caller."""
|
|
88
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
89
|
+
self.config: FNOPeakCallerConfig = config
|
|
90
|
+
|
|
91
|
+
if rngs is None:
|
|
92
|
+
rngs = nnx.Rngs(0)
|
|
93
|
+
|
|
94
|
+
# FNO: 1 input channel (coverage) -> 1 output channel (peak score)
|
|
95
|
+
self.fno = FourierNeuralOperator(
|
|
96
|
+
in_channels=1,
|
|
97
|
+
out_channels=1,
|
|
98
|
+
hidden_channels=config.hidden_channels,
|
|
99
|
+
modes=config.modes,
|
|
100
|
+
num_layers=config.num_layers,
|
|
101
|
+
# Opifex now defaults FNOs to 2D spectral layers; this operator is 1D.
|
|
102
|
+
spatial_dims=1,
|
|
103
|
+
rngs=rngs,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.threshold = nnx.Param(jnp.array(config.threshold))
|
|
107
|
+
|
|
108
|
+
def apply(
|
|
109
|
+
self,
|
|
110
|
+
data: PyTree,
|
|
111
|
+
state: PyTree,
|
|
112
|
+
metadata: dict[str, Any] | None,
|
|
113
|
+
random_params: Any = None, # noqa: ARG002
|
|
114
|
+
stats: dict[str, Any] | None = None, # noqa: ARG002
|
|
115
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
116
|
+
"""Apply FNO peak detection to coverage signal.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
data: Dict with ``"coverage"`` key — shape ``(batch, length)``
|
|
120
|
+
or ``(length,)`` for unbatched.
|
|
121
|
+
state: Element state (passed through).
|
|
122
|
+
metadata: Element metadata (passed through).
|
|
123
|
+
random_params: Unused.
|
|
124
|
+
stats: Unused.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Tuple of (output_data, state, metadata).
|
|
128
|
+
"""
|
|
129
|
+
coverage = data["coverage"]
|
|
130
|
+
unbatched = coverage.ndim == 1
|
|
131
|
+
if unbatched:
|
|
132
|
+
coverage = coverage[None, :]
|
|
133
|
+
|
|
134
|
+
# FNO expects (batch, channels, *spatial_dims) — channels-first
|
|
135
|
+
x = coverage[:, None, :] # (batch, 1, length)
|
|
136
|
+
scores = self.fno(x) # (batch, 1, length)
|
|
137
|
+
scores = scores[:, 0, :] # (batch, length)
|
|
138
|
+
|
|
139
|
+
# Soft peak classification
|
|
140
|
+
temp = self.config.temperature
|
|
141
|
+
probs = soft_ops.greater(scores, self.threshold[...], softness=temp)
|
|
142
|
+
|
|
143
|
+
if unbatched:
|
|
144
|
+
scores = scores.squeeze(0)
|
|
145
|
+
probs = probs.squeeze(0)
|
|
146
|
+
|
|
147
|
+
output_data = {
|
|
148
|
+
**data,
|
|
149
|
+
"peak_scores": scores,
|
|
150
|
+
"peak_probabilities": probs,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return output_data, state, metadata
|