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,332 @@
1
+ """Foundation model infrastructure for single-cell genomics.
2
+
3
+ This module provides differentiable implementations inspired by Geneformer and
4
+ scGPT architectures for single-cell gene expression foundation models.
5
+
6
+ Key components:
7
+
8
+ - **GeneTokenizer**: Geneformer-style rank-value encoding via differentiable
9
+ soft sorting. For each cell, genes are ranked by expression value in
10
+ descending order using a temperature-controlled soft permutation matrix.
11
+ - **DifferentiableFoundationModel**: scGPT-inspired masked gene expression
12
+ model that tokenizes gene expression, embeds gene identities and expression
13
+ values, applies random masking, encodes with a transformer, and predicts
14
+ masked expression values.
15
+
16
+ References:
17
+ - Geneformer: Theodoris et al. (2023) Nature
18
+ - scGPT: Cui et al. (2024) Nature Methods
19
+ """
20
+
21
+ import logging
22
+ from dataclasses import dataclass
23
+ from typing import Any
24
+
25
+ import jax
26
+ import jax.numpy as jnp
27
+ from datarax.core.operator import OperatorModule
28
+ from flax import nnx
29
+ from jaxtyping import Array, Float, PyTree
30
+
31
+ from diffbio.core import soft_ops
32
+ from diffbio.operators._masked_gene_transformer import (
33
+ MaskedGeneTransformerConfigBase,
34
+ MaskedGeneTransformerOperatorMixin,
35
+ build_masked_gene_transformer_encoder,
36
+ )
37
+ from diffbio.operators.foundation_models.contracts import (
38
+ FoundationEmbeddingMixin,
39
+ FoundationEmbeddingOperatorConfig,
40
+ FoundationModelKind,
41
+ PoolingStrategy,
42
+ register_foundation_model,
43
+ )
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class FoundationModelConfig(
50
+ FoundationEmbeddingOperatorConfig,
51
+ MaskedGeneTransformerConfigBase,
52
+ ):
53
+ """Configuration for DifferentiableFoundationModel.
54
+
55
+ Attributes:
56
+ adapter_mode: Integration mode for the model artifact.
57
+ artifact_id: Identifier for the model artifact/version.
58
+ preprocessing_version: Version tag for count/gene-ID preprocessing.
59
+ """
60
+
61
+ artifact_id: str = "diffbio.differentiable_foundation_model"
62
+ preprocessing_version: str = "counts_gene_ids_v1"
63
+
64
+
65
+ class GeneTokenizer(nnx.Module):
66
+ """Geneformer-style rank-value gene tokenizer.
67
+
68
+ Converts gene expression vectors into rank-ordered representations using
69
+ a differentiable soft sort approximation. For each cell, genes are ranked
70
+ by expression value in descending order. The output is a soft permutation
71
+ matrix of shape ``(n_genes, n_genes)`` where row *i* is a soft one-hot
72
+ indicating which gene occupies rank *i*.
73
+
74
+ The key insight from Geneformer: token IDs are gene indices sorted by
75
+ expression magnitude. We approximate the discrete argsort with a
76
+ temperature-controlled soft permutation to maintain differentiability.
77
+
78
+ Args:
79
+ n_genes: Number of genes.
80
+ rngs: Flax NNX random number generators.
81
+ """
82
+
83
+ def __init__(self, n_genes: int, *, rngs: nnx.Rngs) -> None:
84
+ """Initialize the gene tokenizer.
85
+
86
+ Args:
87
+ n_genes: Number of genes in the vocabulary.
88
+ rngs: Random number generators (unused, kept for NNX API).
89
+ """
90
+ super().__init__()
91
+ self.n_genes = n_genes
92
+
93
+ def __call__(
94
+ self,
95
+ expression: Float[Array, "n_genes"],
96
+ temperature: float = 1.0,
97
+ ) -> Float[Array, "n_genes n_genes"]:
98
+ """Compute soft permutation matrix from expression values.
99
+
100
+ Genes are ranked in descending order of expression. The returned
101
+ matrix ``P`` has shape ``(n_genes, n_genes)`` where ``P[i, j]``
102
+ approximates the probability that gene *j* occupies rank *i*.
103
+
104
+ At low temperature this approaches a hard permutation matrix
105
+ (the true argsort).
106
+
107
+ Args:
108
+ expression: Gene expression values for one cell, shape ``(n_genes,)``.
109
+ temperature: Softmax temperature (lower is sharper).
110
+
111
+ Returns:
112
+ Soft permutation matrix of shape ``(n_genes, n_genes)``.
113
+ """
114
+ return _soft_sort_permutation(expression, temperature)
115
+
116
+
117
+ def _soft_sort_permutation(
118
+ values: Float[Array, "n"],
119
+ temperature: float,
120
+ ) -> Float[Array, "n n"]:
121
+ """Compute a differentiable soft permutation for descending sort.
122
+
123
+ Uses the pairwise comparison approach: for each pair (i, j), compute
124
+ a soft indicator of whether ``values[j] > values[i]``. The soft rank
125
+ of gene *j* is the sum of these indicators. A softmax over negative
126
+ squared distance between soft ranks and integer positions yields the
127
+ permutation matrix.
128
+
129
+ Args:
130
+ values: 1-D array of values to sort.
131
+ temperature: Temperature for softmax (lower is sharper).
132
+
133
+ Returns:
134
+ Soft permutation matrix ``P`` of shape ``(n, n)`` where ``P[i, j]``
135
+ approximates the probability that element *j* is at position *i*
136
+ in the descending sort.
137
+ """
138
+ return soft_ops.argsort(values, axis=0, descending=True, softness=temperature)
139
+
140
+
141
+ class DifferentiableFoundationModel(
142
+ FoundationEmbeddingMixin,
143
+ MaskedGeneTransformerOperatorMixin,
144
+ OperatorModule,
145
+ ):
146
+ """Differentiable single-cell foundation model operator.
147
+
148
+ Implements a masked gene expression prediction model inspired by
149
+ Geneformer (rank-value tokenization) and scGPT (masked expression
150
+ prediction with gene + value embeddings).
151
+
152
+ Algorithm:
153
+
154
+ 1. **Tokenize**: Rank genes by expression per cell via soft sort
155
+ (Geneformer-style, used for gene embedding ordering context).
156
+ 2. **Embed gene IDs** via ``TransformerSequenceEncoder`` with
157
+ ``input_embedding_type="token_embedding"``.
158
+ 3. **Add expression value projection**: scalar expression values are
159
+ projected to ``hidden_dim`` and added to gene embeddings (scGPT-style).
160
+ 4. **Random mask**: ``mask_ratio`` fraction of genes have their expression
161
+ embeddings replaced with a learned mask token.
162
+ 5. **Transformer encoder**: contextualizes gene representations.
163
+ 6. **Predict**: linear output head predicts masked gene expression.
164
+ 7. **Cell embedding**: mean pooling of non-masked gene representations.
165
+
166
+ Args:
167
+ config: FoundationModelConfig with model parameters.
168
+ rngs: Flax NNX random number generators.
169
+ name: Optional operator name.
170
+
171
+ Example:
172
+ >>> config = FoundationModelConfig(n_genes=2000, hidden_dim=128)
173
+ >>> model = DifferentiableFoundationModel(
174
+ ... config, rngs=nnx.Rngs(params=0, sample=1, dropout=2))
175
+ >>> rp = model.generate_random_params(
176
+ ... jax.random.key(0), {"counts": (100, 2000)})
177
+ >>> data = {"counts": counts, "gene_ids": jnp.arange(2000)}
178
+ >>> result, state, meta = model.apply(data, {}, None, random_params=rp)
179
+ """
180
+
181
+ foundation_model_kind = FoundationModelKind.SINGLE_CELL_TRANSFORMER
182
+
183
+ def __init__(
184
+ self,
185
+ config: FoundationModelConfig,
186
+ *,
187
+ rngs: nnx.Rngs | None = None,
188
+ name: str | None = None,
189
+ ) -> None:
190
+ """Initialize the foundation model.
191
+
192
+ Args:
193
+ config: Foundation model configuration.
194
+ rngs: Random number generators for parameter initialization.
195
+ name: Optional operator name.
196
+ """
197
+ super().__init__(config, rngs=rngs, name=name)
198
+
199
+ if rngs is None:
200
+ rngs = nnx.Rngs(params=0, sample=1, dropout=2)
201
+
202
+ # Gene tokenizer for rank-value encoding
203
+ self.tokenizer = GeneTokenizer(config.n_genes, rngs=rngs)
204
+
205
+ # Reuse the shared masked-gene token encoder contract.
206
+ self.encoder = build_masked_gene_transformer_encoder(config, rngs=rngs)
207
+
208
+ # Expression value projection: scalar -> hidden_dim (scGPT ContinuousValueEncoder)
209
+ self.expression_projection = nnx.Sequential(
210
+ nnx.Linear(1, config.hidden_dim, rngs=rngs),
211
+ nnx.Linear(config.hidden_dim, config.hidden_dim, rngs=rngs),
212
+ )
213
+
214
+ # Learned mask token embedding (replaces expression embedding for masked genes)
215
+ self.mask_token = nnx.Param(jax.random.normal(rngs.params(), (config.hidden_dim,)) * 0.02)
216
+
217
+ # Output head: hidden_dim -> 1 (predict scalar expression per gene)
218
+ self.output_head = nnx.Linear(config.hidden_dim, 1, rngs=rngs)
219
+
220
+ def foundation_pooling_strategy(self) -> PoolingStrategy:
221
+ """Return the pooling strategy for cell-level embeddings."""
222
+ return PoolingStrategy.MEAN
223
+
224
+ def _process_single_cell(
225
+ self,
226
+ expression: Float[Array, "n_genes"],
227
+ gene_ids: Float[Array, "n_genes"],
228
+ mask: Float[Array, "n_genes"],
229
+ ) -> tuple[
230
+ Float[Array, "n_genes hidden_dim"],
231
+ Float[Array, "hidden_dim"],
232
+ Float[Array, "n_genes"],
233
+ ]:
234
+ """Process a single cell through the foundation model.
235
+
236
+ Args:
237
+ expression: Gene expression values for one cell.
238
+ gene_ids: Integer gene IDs.
239
+ mask: Binary mask (1 = masked, 0 = observed).
240
+
241
+ Returns:
242
+ Tuple of (gene_representations, cell_embedding, predicted_expression).
243
+ """
244
+ # Step 1: Embed gene IDs via the encoder's input projection (nnx.Embed)
245
+ gene_embeddings = self.encoder.input_projection(gene_ids) # (n_genes, hidden_dim)
246
+
247
+ # Step 2: Project expression values to hidden_dim (scGPT-style value encoder)
248
+ expr_projected = self.expression_projection(expression[:, None]) # (n_genes, hidden_dim)
249
+
250
+ # Step 3: Apply mask -- replace masked expression embeddings with mask token
251
+ mask_expanded = mask[:, None] # (n_genes, 1)
252
+ mask_token_broadcast = jnp.broadcast_to(self.mask_token[...][None, :], expr_projected.shape)
253
+ expr_projected = jnp.where(mask_expanded > 0.5, mask_token_broadcast, expr_projected)
254
+
255
+ # Step 4: Combine gene identity embeddings + expression value embeddings
256
+ hidden = gene_embeddings + expr_projected # (n_genes, hidden_dim)
257
+
258
+ # Step 5: Apply transformer encoder
259
+ hidden = hidden[None, :, :] # (1, n_genes, hidden_dim)
260
+ hidden = self.encoder.transformer(hidden, mask=None, deterministic=True)
261
+ hidden = hidden[0] # (n_genes, hidden_dim)
262
+
263
+ # Step 6: Predict expression for all genes via output head
264
+ predicted = self.output_head(hidden).squeeze(-1) # (n_genes,)
265
+
266
+ # Step 7: Cell embedding = mean pooling of non-masked gene representations
267
+ # Use (1 - mask) to select non-masked genes
268
+ non_masked_weight = (1.0 - mask)[:, None] # (n_genes, 1)
269
+ non_masked_count = jnp.maximum(jnp.sum(1.0 - mask), 1.0)
270
+ cell_embedding = (
271
+ jnp.sum(hidden * non_masked_weight, axis=0) / non_masked_count
272
+ ) # (hidden_dim,)
273
+
274
+ return hidden, cell_embedding, predicted
275
+
276
+ def apply(
277
+ self,
278
+ data: PyTree,
279
+ state: PyTree,
280
+ metadata: dict[str, Any] | None,
281
+ random_params: Any = None,
282
+ stats: dict[str, Any] | None = None,
283
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
284
+ """Apply foundation model to single-cell count data.
285
+
286
+ Args:
287
+ data: Dictionary containing:
288
+ - ``"counts"``: Gene expression matrix ``(n_cells, n_genes)``
289
+ - ``"gene_ids"``: Integer gene IDs ``(n_genes,)``
290
+ state: Element state (passed through unchanged).
291
+ metadata: Element metadata (passed through unchanged).
292
+ random_params: JAX random key for mask generation.
293
+ stats: Not used.
294
+
295
+ Returns:
296
+ Tuple of (transformed_data, state, metadata):
297
+ - transformed_data contains:
298
+
299
+ - All original keys from data
300
+ - ``"embeddings"``: Cell embeddings ``(n_cells, hidden_dim)``
301
+ - ``"token_embeddings"``: Contextual gene embeddings
302
+ ``(n_cells, n_genes, hidden_dim)``
303
+ - ``"predicted_expression"``: Predicted expression ``(n_cells, n_genes)``
304
+ - ``"foundation_model"``: Canonical artifact metadata
305
+ - state is passed through unchanged
306
+ - metadata is passed through unchanged
307
+ """
308
+ counts, gene_ids_int, mask = self.prepare_masked_gene_batch(data, random_params)
309
+
310
+ # Process each cell independently via vmap
311
+ gene_reps, embeddings, predicted = jax.vmap(
312
+ self._process_single_cell,
313
+ in_axes=(0, None, None),
314
+ )(counts, gene_ids_int, mask)
315
+ # gene_reps: (n_cells, n_genes, hidden_dim)
316
+ # embeddings: (n_cells, hidden_dim)
317
+ # predicted: (n_cells, n_genes)
318
+
319
+ transformed_data = self.foundation_result(
320
+ data,
321
+ embeddings,
322
+ token_embeddings=gene_reps,
323
+ extra_outputs={"predicted_expression": predicted},
324
+ )
325
+
326
+ return transformed_data, state, metadata
327
+
328
+
329
+ register_foundation_model(
330
+ FoundationModelKind.SINGLE_CELL_TRANSFORMER,
331
+ DifferentiableFoundationModel,
332
+ )
@@ -0,0 +1,59 @@
1
+ """Frozen in-process adapters for benchmarked foundation-model integrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any
7
+
8
+ import jax.numpy as jnp
9
+ from flax import nnx
10
+
11
+ from diffbio.operators.foundation_models.adapters import (
12
+ FoundationBenchmarkAdapterBase,
13
+ register_foundation_adapter,
14
+ )
15
+ from diffbio.operators.foundation_models.contracts import AdapterMode
16
+ from diffbio.operators.foundation_models.transformer_encoder import (
17
+ TransformerSequenceEncoder,
18
+ TransformerSequenceEncoderConfig,
19
+ )
20
+
21
+
22
+ class FrozenSequenceEncoderAdapter(FoundationBenchmarkAdapterBase):
23
+ """Benchmark adapter for a frozen in-process sequence encoder."""
24
+
25
+ def __init__(
26
+ self,
27
+ *,
28
+ config: TransformerSequenceEncoderConfig,
29
+ rngs: nnx.Rngs | None = None,
30
+ source_name: str = "diffbio_frozen_encoder",
31
+ ) -> None:
32
+ if config.adapter_mode is not AdapterMode.FROZEN_ENCODER:
33
+ raise ValueError("FrozenSequenceEncoderAdapter requires adapter_mode='frozen_encoder'.")
34
+
35
+ self.encoder = TransformerSequenceEncoder(config, rngs=rngs)
36
+ super().__init__(
37
+ artifact_spec=self.encoder.foundation_artifact_spec(),
38
+ source_name=source_name,
39
+ embedding_source="in_process_operator",
40
+ )
41
+
42
+ def load_dataset_embeddings(
43
+ self,
44
+ *,
45
+ reference_sequence_ids: Sequence[str],
46
+ one_hot_sequences: Any,
47
+ ) -> jnp.ndarray:
48
+ """Encode a benchmark dataset in-process without updating encoder weights."""
49
+ sequences = jnp.asarray(one_hot_sequences, dtype=jnp.float32)
50
+ if sequences.shape[0] != len(reference_sequence_ids):
51
+ raise ValueError(
52
+ "reference_sequence_ids and one_hot_sequences "
53
+ "must share the same leading dimension."
54
+ )
55
+ result, _, _ = self.encoder.apply({"sequence": sequences}, {}, None)
56
+ return jnp.asarray(result["embeddings"], dtype=jnp.float32)
57
+
58
+
59
+ register_foundation_adapter("diffbio_frozen_encoder", FrozenSequenceEncoderAdapter)
@@ -0,0 +1,270 @@
1
+ """Precomputed artifact adapters for imported foundation-model embeddings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from collections.abc import Sequence
7
+ from typing import Any
8
+
9
+ import jax.numpy as jnp
10
+
11
+ from diffbio.operators.foundation_models.adapters import (
12
+ FoundationBenchmarkAdapterBase,
13
+ register_foundation_adapter,
14
+ )
15
+ from diffbio.operators.foundation_models.contracts import (
16
+ AdapterMode,
17
+ FoundationArtifactSpec,
18
+ FoundationModelKind,
19
+ PoolingStrategy,
20
+ )
21
+ from diffbio.sources.sequence_foundation import align_sequence_embeddings
22
+ from diffbio.sources.singlecell_foundation import align_singlecell_embeddings
23
+
24
+
25
+ def _singlecell_precomputed_spec(
26
+ *,
27
+ artifact_id: str,
28
+ preprocessing_version: str,
29
+ pooling_strategy: PoolingStrategy,
30
+ ) -> FoundationArtifactSpec:
31
+ """Build the canonical artifact spec for imported single-cell embeddings."""
32
+ return FoundationArtifactSpec(
33
+ model_family=FoundationModelKind.SINGLE_CELL_TRANSFORMER,
34
+ artifact_id=artifact_id,
35
+ preprocessing_version=preprocessing_version,
36
+ adapter_mode=AdapterMode.PRECOMPUTED,
37
+ pooling_strategy=pooling_strategy,
38
+ )
39
+
40
+
41
+ def _sequence_precomputed_spec(
42
+ *,
43
+ artifact_id: str,
44
+ preprocessing_version: str,
45
+ pooling_strategy: PoolingStrategy,
46
+ ) -> FoundationArtifactSpec:
47
+ """Build the canonical artifact spec for imported sequence embeddings."""
48
+ return FoundationArtifactSpec(
49
+ model_family=FoundationModelKind.SEQUENCE_TRANSFORMER,
50
+ artifact_id=artifact_id,
51
+ preprocessing_version=preprocessing_version,
52
+ adapter_mode=AdapterMode.PRECOMPUTED,
53
+ pooling_strategy=pooling_strategy,
54
+ )
55
+
56
+
57
+ class _ArtifactBackedFoundationAdapter(FoundationBenchmarkAdapterBase):
58
+ """Shared base for artifact-backed imported foundation-model adapters."""
59
+
60
+ def __init__(
61
+ self,
62
+ *,
63
+ artifact_path: Path | str,
64
+ artifact_spec: FoundationArtifactSpec,
65
+ source_name: str,
66
+ extra_metadata: dict[str, Any] | None = None,
67
+ ) -> None:
68
+ super().__init__(
69
+ artifact_spec=artifact_spec,
70
+ source_name=source_name,
71
+ embedding_source="external_artifact",
72
+ extra_metadata=extra_metadata,
73
+ )
74
+ self.artifact_path = Path(artifact_path)
75
+
76
+ def load_aligned_embeddings(
77
+ self,
78
+ *,
79
+ reference_cell_ids: list[str] | tuple[str, ...],
80
+ require_cell_ids: bool = True,
81
+ ) -> jnp.ndarray:
82
+ """Load and align embeddings to the benchmark dataset order."""
83
+ return align_singlecell_embeddings(
84
+ reference_cell_ids=reference_cell_ids,
85
+ artifact_path=self.artifact_path,
86
+ require_cell_ids=require_cell_ids,
87
+ )
88
+
89
+
90
+ class SingleCellPrecomputedAdapter(_ArtifactBackedFoundationAdapter):
91
+ """Base adapter for precomputed single-cell embedding artifacts."""
92
+
93
+ def load_aligned_embeddings(
94
+ self,
95
+ *,
96
+ reference_cell_ids: list[str] | tuple[str, ...],
97
+ require_cell_ids: bool = True,
98
+ ) -> jnp.ndarray:
99
+ """Load and align embeddings to the benchmark dataset order."""
100
+ return align_singlecell_embeddings(
101
+ reference_cell_ids=reference_cell_ids,
102
+ artifact_path=self.artifact_path,
103
+ require_cell_ids=require_cell_ids,
104
+ )
105
+
106
+
107
+ class SequencePrecomputedAdapter(_ArtifactBackedFoundationAdapter):
108
+ """Base adapter for precomputed sequence embedding artifacts."""
109
+
110
+ def load_aligned_embeddings(
111
+ self,
112
+ *,
113
+ reference_sequence_ids: list[str] | tuple[str, ...],
114
+ require_sequence_ids: bool = True,
115
+ ) -> jnp.ndarray:
116
+ """Load and align embeddings to the benchmark sequence order."""
117
+ return align_sequence_embeddings(
118
+ reference_sequence_ids=reference_sequence_ids,
119
+ artifact_path=self.artifact_path,
120
+ require_sequence_ids=require_sequence_ids,
121
+ )
122
+
123
+ def load_dataset_embeddings(
124
+ self,
125
+ *,
126
+ reference_sequence_ids: Sequence[str],
127
+ one_hot_sequences: Any,
128
+ ) -> jnp.ndarray:
129
+ """Load embeddings for a sequence benchmark dataset."""
130
+ n_sequences = int(jnp.asarray(one_hot_sequences).shape[0])
131
+ if n_sequences != len(reference_sequence_ids):
132
+ raise ValueError(
133
+ "reference_sequence_ids and one_hot_sequences "
134
+ "must share the same leading dimension."
135
+ )
136
+ return self.load_aligned_embeddings(
137
+ reference_sequence_ids=list(reference_sequence_ids),
138
+ require_sequence_ids=True,
139
+ )
140
+
141
+
142
+ class DNABERT2PrecomputedAdapter(SequencePrecomputedAdapter):
143
+ """Precomputed embedding adapter for DNABERT-2 artifacts."""
144
+
145
+ def __init__(
146
+ self,
147
+ *,
148
+ artifact_path: Path | str,
149
+ artifact_id: str = "dnabert2.v1",
150
+ preprocessing_version: str = "kmer6_v1",
151
+ pooling_strategy: PoolingStrategy = PoolingStrategy.MEAN,
152
+ ) -> None:
153
+ super().__init__(
154
+ artifact_path=artifact_path,
155
+ artifact_spec=_sequence_precomputed_spec(
156
+ artifact_id=artifact_id,
157
+ preprocessing_version=preprocessing_version,
158
+ pooling_strategy=pooling_strategy,
159
+ ),
160
+ source_name="dnabert2_precomputed",
161
+ )
162
+
163
+
164
+ class NucleotideTransformerPrecomputedAdapter(SequencePrecomputedAdapter):
165
+ """Precomputed embedding adapter for Nucleotide Transformer artifacts."""
166
+
167
+ def __init__(
168
+ self,
169
+ *,
170
+ artifact_path: Path | str,
171
+ artifact_id: str = "nucleotide_transformer.v1",
172
+ preprocessing_version: str = "bpe_v1",
173
+ pooling_strategy: PoolingStrategy = PoolingStrategy.MEAN,
174
+ ) -> None:
175
+ super().__init__(
176
+ artifact_path=artifact_path,
177
+ artifact_spec=_sequence_precomputed_spec(
178
+ artifact_id=artifact_id,
179
+ preprocessing_version=preprocessing_version,
180
+ pooling_strategy=pooling_strategy,
181
+ ),
182
+ source_name="nucleotide_transformer_precomputed",
183
+ )
184
+
185
+
186
+ class ProteinLMPrecomputedAdapter(SequencePrecomputedAdapter):
187
+ """Precomputed embedding adapter for exported protein language-model artifacts."""
188
+
189
+ def __init__(
190
+ self,
191
+ *,
192
+ artifact_path: Path | str,
193
+ artifact_id: str = "protein_lm.v1",
194
+ preprocessing_version: str = "protein_tokens_v1",
195
+ pooling_strategy: PoolingStrategy = PoolingStrategy.MEAN,
196
+ ) -> None:
197
+ super().__init__(
198
+ artifact_path=artifact_path,
199
+ artifact_spec=_sequence_precomputed_spec(
200
+ artifact_id=artifact_id,
201
+ preprocessing_version=preprocessing_version,
202
+ pooling_strategy=pooling_strategy,
203
+ ),
204
+ source_name="protein_lm_precomputed",
205
+ )
206
+
207
+
208
+ class GeneformerPrecomputedAdapter(SingleCellPrecomputedAdapter):
209
+ """Precomputed embedding adapter for Geneformer artifacts."""
210
+
211
+ def __init__(
212
+ self,
213
+ *,
214
+ artifact_path: Path | str,
215
+ artifact_id: str = "geneformer.v1",
216
+ preprocessing_version: str = "rank_value_v1",
217
+ pooling_strategy: PoolingStrategy = PoolingStrategy.MEAN,
218
+ ) -> None:
219
+ super().__init__(
220
+ artifact_path=artifact_path,
221
+ artifact_spec=_singlecell_precomputed_spec(
222
+ artifact_id=artifact_id,
223
+ preprocessing_version=preprocessing_version,
224
+ pooling_strategy=pooling_strategy,
225
+ ),
226
+ source_name="geneformer_precomputed",
227
+ )
228
+
229
+
230
+ class ScGPTPrecomputedAdapter(SingleCellPrecomputedAdapter):
231
+ """Precomputed embedding adapter for scGPT artifacts."""
232
+
233
+ def __init__(
234
+ self,
235
+ *,
236
+ artifact_path: Path | str,
237
+ artifact_id: str = "scgpt.v1",
238
+ preprocessing_version: str = "gene_vocab_v1",
239
+ pooling_strategy: PoolingStrategy = PoolingStrategy.MEAN,
240
+ batch_key: str | None = None,
241
+ context_version: str | None = None,
242
+ ) -> None:
243
+ extra_metadata: dict[str, Any] = {
244
+ "requires_batch_context": batch_key is not None or context_version is not None,
245
+ }
246
+ if batch_key is not None:
247
+ extra_metadata["batch_key"] = batch_key
248
+ if context_version is not None:
249
+ extra_metadata["context_version"] = context_version
250
+
251
+ super().__init__(
252
+ artifact_path=artifact_path,
253
+ artifact_spec=_singlecell_precomputed_spec(
254
+ artifact_id=artifact_id,
255
+ preprocessing_version=preprocessing_version,
256
+ pooling_strategy=pooling_strategy,
257
+ ),
258
+ source_name="scgpt_precomputed",
259
+ extra_metadata=extra_metadata,
260
+ )
261
+
262
+
263
+ register_foundation_adapter("dnabert2_precomputed", DNABERT2PrecomputedAdapter)
264
+ register_foundation_adapter(
265
+ "nucleotide_transformer_precomputed",
266
+ NucleotideTransformerPrecomputedAdapter,
267
+ )
268
+ register_foundation_adapter("protein_lm_precomputed", ProteinLMPrecomputedAdapter)
269
+ register_foundation_adapter("geneformer_precomputed", GeneformerPrecomputedAdapter)
270
+ register_foundation_adapter("scgpt_precomputed", ScGPTPrecomputedAdapter)