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,118 @@
1
+ """Shared scaffolding for masked-gene transformer operators.
2
+
3
+ These helpers centralize the common single-cell transformer setup used by the
4
+ foundation-model and imputation operators so both paths share one encoder
5
+ construction and one mask/input preparation flow.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ import jax
14
+ import jax.numpy as jnp
15
+ from datarax.core.config import OperatorConfig
16
+ from flax import nnx
17
+ from jaxtyping import Array, PyTree
18
+
19
+ from diffbio.operators.foundation_models.transformer_encoder import (
20
+ TransformerSequenceEncoder,
21
+ TransformerSequenceEncoderConfig,
22
+ )
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class MaskedGeneTransformerConfigBase(OperatorConfig):
27
+ """Shared config fields for masked-gene transformer operators."""
28
+
29
+ n_genes: int = 2000
30
+ hidden_dim: int = 128
31
+ num_layers: int = 2
32
+ num_heads: int = 4
33
+ mask_ratio: float = 0.15
34
+ dropout_rate: float = 0.1
35
+
36
+ def __post_init__(self) -> None:
37
+ """Default masked-gene operators to sampled stochastic execution."""
38
+ object.__setattr__(self, "stochastic", True)
39
+ if self.stream_name is None:
40
+ object.__setattr__(self, "stream_name", "sample")
41
+ super().__post_init__()
42
+
43
+
44
+ def build_masked_gene_transformer_encoder(
45
+ config: MaskedGeneTransformerConfigBase,
46
+ *,
47
+ rngs: nnx.Rngs,
48
+ ) -> TransformerSequenceEncoder:
49
+ """Build the shared token-embedding transformer encoder contract."""
50
+ encoder_config = TransformerSequenceEncoderConfig(
51
+ hidden_dim=config.hidden_dim,
52
+ num_layers=config.num_layers,
53
+ num_heads=config.num_heads,
54
+ intermediate_dim=4 * config.hidden_dim,
55
+ max_length=config.n_genes,
56
+ input_embedding_type="token_embedding",
57
+ vocab_size=config.n_genes,
58
+ dropout_rate=config.dropout_rate,
59
+ pooling="mean",
60
+ )
61
+ return TransformerSequenceEncoder(encoder_config, rngs=rngs)
62
+
63
+
64
+ def build_masked_gene_mask(
65
+ *,
66
+ random_params: Any,
67
+ mask_ratio: float,
68
+ n_genes: int,
69
+ ) -> Array:
70
+ """Build a per-gene binary mask for masked-gene transformer operators."""
71
+ if random_params is not None and mask_ratio > 0:
72
+ noise = jax.random.uniform(random_params, (n_genes,))
73
+ return (noise < mask_ratio).astype(jnp.float32)
74
+ return jnp.zeros(n_genes, dtype=jnp.float32)
75
+
76
+
77
+ def prepare_masked_gene_batch(
78
+ data: PyTree,
79
+ *,
80
+ random_params: Any,
81
+ mask_ratio: float,
82
+ ) -> tuple[Array, Array, Array]:
83
+ """Extract counts, int32 gene IDs, and the shared masking vector."""
84
+ counts = data["counts"]
85
+ gene_ids = jnp.asarray(data["gene_ids"], dtype=jnp.int32)
86
+ mask = build_masked_gene_mask(
87
+ random_params=random_params,
88
+ mask_ratio=mask_ratio,
89
+ n_genes=int(counts.shape[1]),
90
+ )
91
+ return counts, gene_ids, mask
92
+
93
+
94
+ class MaskedGeneTransformerOperatorMixin:
95
+ """Mixin for operators built on the shared masked-gene transformer flow."""
96
+
97
+ config: MaskedGeneTransformerConfigBase
98
+
99
+ def generate_random_params(
100
+ self,
101
+ rng: jax.Array,
102
+ data_shapes: PyTree,
103
+ ) -> jax.Array:
104
+ """Return the RNG key used for reproducible masking inside apply."""
105
+ del data_shapes
106
+ return rng
107
+
108
+ def prepare_masked_gene_batch(
109
+ self,
110
+ data: PyTree,
111
+ random_params: Any,
112
+ ) -> tuple[Array, Array, Array]:
113
+ """Prepare shared masked-gene inputs for per-cell `vmap` execution."""
114
+ return prepare_masked_gene_batch(
115
+ data,
116
+ random_params=random_params,
117
+ mask_ratio=self.config.mask_ratio,
118
+ )
@@ -0,0 +1,50 @@
1
+ """Shared validation for transformer-style operator configs."""
2
+
3
+
4
+ def validate_transformer_encoder_shape(
5
+ *,
6
+ hidden_dim: int,
7
+ num_layers: int,
8
+ num_heads: int,
9
+ intermediate_dim: int,
10
+ max_length: int,
11
+ dropout_rate: float,
12
+ ) -> None:
13
+ """Validate common transformer encoder hyperparameters."""
14
+ if hidden_dim <= 0:
15
+ raise ValueError("hidden_dim must be positive.")
16
+ if num_layers <= 0:
17
+ raise ValueError("num_layers must be positive.")
18
+ if num_heads <= 0:
19
+ raise ValueError("num_heads must be positive.")
20
+ if hidden_dim % num_heads != 0:
21
+ raise ValueError("hidden_dim must be divisible by num_heads.")
22
+ if intermediate_dim <= 0:
23
+ raise ValueError("intermediate_dim must be positive.")
24
+ if max_length <= 0:
25
+ raise ValueError("max_length must be positive.")
26
+ if not 0.0 <= dropout_rate < 1.0:
27
+ raise ValueError("dropout_rate must be in [0.0, 1.0).")
28
+
29
+
30
+ class TransformerEncoderShapeValidationMixin:
31
+ """Mixin that validates shared transformer encoder dimensions."""
32
+
33
+ hidden_dim: int
34
+ num_layers: int
35
+ num_heads: int
36
+ intermediate_dim: int
37
+ max_length: int
38
+ dropout_rate: float
39
+
40
+ def __post_init__(self) -> None:
41
+ """Run base config validation and shared transformer checks."""
42
+ super().__post_init__()
43
+ validate_transformer_encoder_shape(
44
+ hidden_dim=self.hidden_dim,
45
+ num_layers=self.num_layers,
46
+ num_heads=self.num_heads,
47
+ intermediate_dim=self.intermediate_dim,
48
+ max_length=self.max_length,
49
+ dropout_rate=self.dropout_rate,
50
+ )
@@ -0,0 +1,51 @@
1
+ """Differentiable sequence alignment operators.
2
+
3
+ This module provides differentiable implementations of sequence alignment
4
+ algorithms including smooth Smith-Waterman for local alignment,
5
+ profile HMM search for domain detection, and soft progressive MSA.
6
+ """
7
+
8
+ from diffbio.operators.alignment.profile_hmm import (
9
+ ProfileHMMConfig,
10
+ ProfileHMMSearch,
11
+ )
12
+ from diffbio.operators.alignment.scoring import (
13
+ PROTEIN_ALPHABET,
14
+ ScoringMatrix,
15
+ create_dna_scoring_matrix,
16
+ create_rna_scoring_matrix,
17
+ get_blosum62,
18
+ get_dna_simple,
19
+ get_rna_simple,
20
+ )
21
+ from diffbio.operators.alignment.smith_waterman import (
22
+ AlignmentResult,
23
+ SmithWatermanConfig,
24
+ SmoothSmithWaterman,
25
+ )
26
+ from diffbio.operators.alignment.soft_msa import (
27
+ SoftProgressiveMSA,
28
+ SoftProgressiveMSAConfig,
29
+ )
30
+
31
+
32
+ __all__ = [
33
+ # Profile HMM
34
+ "ProfileHMMConfig",
35
+ "ProfileHMMSearch",
36
+ # Scoring
37
+ "PROTEIN_ALPHABET",
38
+ "ScoringMatrix",
39
+ "create_dna_scoring_matrix",
40
+ "create_rna_scoring_matrix",
41
+ "get_blosum62",
42
+ "get_dna_simple",
43
+ "get_rna_simple",
44
+ # Smith-Waterman
45
+ "AlignmentResult",
46
+ "SmithWatermanConfig",
47
+ "SmoothSmithWaterman",
48
+ # Soft MSA
49
+ "SoftProgressiveMSA",
50
+ "SoftProgressiveMSAConfig",
51
+ ]
@@ -0,0 +1,350 @@
1
+ """Profile HMM search operator for HMMER-style sequence alignment.
2
+
3
+ This module provides a differentiable implementation of profile HMM
4
+ search, enabling gradient-based learning of profile parameters.
5
+
6
+ Key technique: Use the forward algorithm with logsumexp for
7
+ differentiable profile-sequence alignment scoring.
8
+
9
+ Applications: Protein domain detection, remote homology search.
10
+
11
+ Inherits from TemperatureOperator to get:
12
+
13
+ - _temperature property for temperature-controlled smoothing
14
+ - soft_max() for logsumexp-based smooth maximum
15
+ - soft_argmax() for soft position selection
16
+ """
17
+
18
+ import logging
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+
22
+ import jax
23
+ import jax.numpy as jnp
24
+ from datarax.core.config import OperatorConfig
25
+ from flax import nnx
26
+ from jaxtyping import Array, Float, PyTree
27
+
28
+ from diffbio.core.base_operators import TemperatureOperator
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ProfileHMMConfig(OperatorConfig):
35
+ """Configuration for ProfileHMMSearch.
36
+
37
+ Attributes:
38
+ profile_length: Length of the profile (number of match states).
39
+ alphabet_size: Size of sequence alphabet (20 for protein, 4 for DNA).
40
+ temperature: Temperature for softmax operations.
41
+ learnable_profile: Whether profile parameters are learnable.
42
+ """
43
+
44
+ cacheable: bool = True
45
+ profile_length: int = 100
46
+ alphabet_size: int = 20 # Amino acids by default
47
+ temperature: float = 1.0
48
+ learnable_profile: bool = True
49
+
50
+
51
+ class ProfileHMMSearch(TemperatureOperator):
52
+ """Profile HMM search with differentiable scoring.
53
+
54
+ This operator implements a simplified profile HMM with match, insert,
55
+ and delete states. The forward algorithm computes the alignment score
56
+ differentiably using logsumexp.
57
+
58
+ Profile HMM structure (per position):
59
+ - Match state: emits according to position-specific distribution
60
+ - Insert state: emits according to background distribution
61
+ - Delete state: silent (no emission)
62
+
63
+ Transitions:
64
+ - M->M, M->I, M->D (from match)
65
+ - I->M, I->I (from insert)
66
+ - D->M, D->D (from delete)
67
+
68
+ Inherits from TemperatureOperator to get:
69
+
70
+ - _temperature property for temperature-controlled smoothing
71
+ - soft_max() for logsumexp-based smooth maximum
72
+ - soft_argmax() for soft position selection
73
+
74
+ Args:
75
+ config: ProfileHMMConfig with model parameters.
76
+ rngs: Flax NNX random number generators.
77
+ name: Optional operator name.
78
+
79
+ Example:
80
+ ```python
81
+ config = ProfileHMMConfig(profile_length=100, alphabet_size=20)
82
+ profiler = ProfileHMMSearch(config, rngs=nnx.Rngs(42))
83
+ data = {"sequence": one_hot_sequence}
84
+ result, state, meta = profiler.apply(data, {}, None)
85
+ ```
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ config: ProfileHMMConfig,
91
+ *,
92
+ rngs: nnx.Rngs | None = None,
93
+ name: str | None = None,
94
+ ):
95
+ """Initialize the profile HMM operator.
96
+
97
+ Args:
98
+ config: Profile HMM configuration.
99
+ rngs: Random number generators for initialization.
100
+ name: Optional operator name.
101
+ """
102
+ super().__init__(config, rngs=rngs, name=name)
103
+
104
+ if rngs is None:
105
+ rngs = nnx.Rngs(0)
106
+
107
+ self.profile_length = config.profile_length
108
+ self.alphabet_size = config.alphabet_size
109
+ # Temperature is now managed by TemperatureOperator via self._temperature
110
+
111
+ # Initialize match emissions (profile_length, alphabet_size)
112
+ key = rngs.params()
113
+ init_match = jax.random.normal(key, (config.profile_length, config.alphabet_size)) * 0.1
114
+ self.log_match_emissions = nnx.Param(init_match)
115
+
116
+ # Initialize insert emissions (profile_length, alphabet_size)
117
+ # Insert states use near-uniform distribution
118
+ key = rngs.params()
119
+ init_insert = jax.random.normal(key, (config.profile_length, config.alphabet_size)) * 0.01
120
+ self.log_insert_emissions = nnx.Param(init_insert)
121
+
122
+ # Initialize transition parameters
123
+ # For each position: [M->M, M->I, M->D, I->M, I->I, D->M, D->D]
124
+ key = rngs.params()
125
+ # Default: prefer M->M transitions
126
+ init_trans = jnp.zeros((config.profile_length, 7))
127
+ init_trans = init_trans.at[:, 0].set(2.0) # M->M bias
128
+ init_trans = init_trans.at[:, 3].set(1.0) # I->M bias
129
+ init_trans = init_trans.at[:, 5].set(1.0) # D->M bias
130
+ init_trans = init_trans + jax.random.normal(key, init_trans.shape) * 0.1
131
+ self.log_transitions = nnx.Param(init_trans)
132
+
133
+ def get_match_emissions(self) -> Float[Array, "profile_length alphabet_size"]:
134
+ """Get normalized match emission probabilities.
135
+
136
+ Returns:
137
+ Log match emission probabilities.
138
+ """
139
+ return jax.nn.log_softmax(self.log_match_emissions[...] / self._temperature, axis=1)
140
+
141
+ def get_insert_emissions(self) -> Float[Array, "profile_length alphabet_size"]:
142
+ """Get normalized insert emission probabilities.
143
+
144
+ Returns:
145
+ Log insert emission probabilities.
146
+ """
147
+ return jax.nn.log_softmax(self.log_insert_emissions[...] / self._temperature, axis=1)
148
+
149
+ def get_transitions(self) -> dict[str, Float[Array, "profile_length"]]:
150
+ """Get normalized transition probabilities.
151
+
152
+ Returns:
153
+ Dictionary with log transition probabilities for each type.
154
+ """
155
+ trans = self.log_transitions[...]
156
+
157
+ # Normalize M->* transitions (positions 0, 1, 2)
158
+ log_m_trans = jax.nn.log_softmax(trans[:, :3] / self._temperature, axis=1)
159
+
160
+ # Normalize I->* transitions (positions 3, 4)
161
+ log_i_trans = jax.nn.log_softmax(trans[:, 3:5] / self._temperature, axis=1)
162
+
163
+ # Normalize D->* transitions (positions 5, 6)
164
+ log_d_trans = jax.nn.log_softmax(trans[:, 5:7] / self._temperature, axis=1)
165
+
166
+ return {
167
+ "m_to_m": log_m_trans[:, 0],
168
+ "m_to_i": log_m_trans[:, 1],
169
+ "m_to_d": log_m_trans[:, 2],
170
+ "i_to_m": log_i_trans[:, 0],
171
+ "i_to_i": log_i_trans[:, 1],
172
+ "d_to_m": log_d_trans[:, 0],
173
+ "d_to_d": log_d_trans[:, 1],
174
+ }
175
+
176
+ def score_sequence(
177
+ self,
178
+ sequence: Float[Array, "seq_len alphabet_size"],
179
+ ) -> Float[Array, ""]:
180
+ """Score a sequence against the profile using forward algorithm.
181
+
182
+ Computes log P(sequence | profile) using dynamic programming.
183
+
184
+ Args:
185
+ sequence: One-hot encoded sequence.
186
+
187
+ Returns:
188
+ Log probability (alignment score).
189
+ """
190
+ seq_len = sequence.shape[0]
191
+ log_match = self.get_match_emissions()
192
+ log_insert = self.get_insert_emissions()
193
+ trans = self.get_transitions()
194
+
195
+ # DP matrices: (seq_pos, profile_pos)
196
+ # We use a simplified forward algorithm
197
+
198
+ # Emission scores for each sequence position at each profile position
199
+ # match_scores[i, j] = log P(seq[i] | Match[j])
200
+ match_scores = jnp.einsum("sa,pa->sp", sequence, jnp.exp(log_match))
201
+ match_scores = jnp.log(match_scores + 1e-10)
202
+
203
+ # Insert scores per position (use position-specific insert emissions)
204
+ insert_scores = jnp.einsum("sa,pa->sp", sequence, jnp.exp(log_insert))
205
+ insert_scores = jnp.log(insert_scores + 1e-10)
206
+
207
+ # Initialize DP
208
+ # log_M[j] = log probability of being in Match state j
209
+ # log_I[j] = log probability of being in Insert state j
210
+ # log_D[j] = log probability of being in Delete state j
211
+
212
+ neg_inf = -1e10
213
+
214
+ # Initial state: can start at any match state with decreasing probability
215
+ # or go through leading deletes
216
+ log_M = jnp.full(self.profile_length, neg_inf)
217
+ log_I = jnp.full(self.profile_length, neg_inf)
218
+ log_D = jnp.full(self.profile_length, neg_inf)
219
+
220
+ # Start: emit first sequence position at first match state
221
+ log_M = log_M.at[0].set(match_scores[0, 0])
222
+ log_I = log_I.at[0].set(insert_scores[0, 0])
223
+
224
+ # Forward pass
225
+ def forward_step(carry, seq_idx):
226
+ log_M, log_I, log_D = carry
227
+ obs_match = match_scores[seq_idx]
228
+ obs_insert = insert_scores[seq_idx]
229
+
230
+ # New M states: can come from M, I, or D at previous profile position
231
+ # M[j] <- M[j-1] + M->M + emit[j]
232
+ # <- I[j-1] + I->M + emit[j]
233
+ # <- D[j-1] + D->M + emit[j]
234
+ new_log_M = jnp.full(self.profile_length, neg_inf)
235
+
236
+ # From previous M (M[j-1] -> M[j] for j=1..L-1)
237
+ from_M = log_M[:-1] + trans["m_to_m"][:-1]
238
+ # From previous I (I[j-1] -> M[j] for j=1..L-1)
239
+ from_I = log_I[:-1] + trans["i_to_m"][:-1]
240
+ # From previous D (D[j-1] -> M[j] for j=1..L-1)
241
+ from_D = log_D[:-1] + trans["d_to_m"][:-1]
242
+
243
+ # Combine and add emission
244
+ combined = jax.scipy.special.logsumexp(jnp.stack([from_M, from_I, from_D]), axis=0)
245
+ new_log_M = new_log_M.at[1:].set(combined + obs_match[1:])
246
+
247
+ # Can also start fresh at position 0
248
+ new_log_M = new_log_M.at[0].set(
249
+ jax.scipy.special.logsumexp(jnp.array([new_log_M[0], obs_match[0]]))
250
+ )
251
+
252
+ # New I states: can come from M or I at same profile position
253
+ from_M_to_I = log_M + trans["m_to_i"]
254
+ from_I_to_I = log_I + trans["i_to_i"]
255
+ new_log_I = (
256
+ jax.scipy.special.logsumexp(jnp.stack([from_M_to_I, from_I_to_I]), axis=0)
257
+ + obs_insert
258
+ )
259
+
260
+ # D states don't emit, handled separately
261
+ # For simplicity, we skip explicit D state tracking in emissions
262
+ # and just allow gaps through the transition structure
263
+ new_log_D = log_D # Simplified: D states updated via M transitions
264
+
265
+ return (new_log_M, new_log_I, new_log_D), None
266
+
267
+ # Scan over sequence positions (skip first, handled in init)
268
+ (final_M, final_I, final_D), _ = jax.lax.scan(
269
+ forward_step, (log_M, log_I, log_D), jnp.arange(1, seq_len)
270
+ )
271
+
272
+ # Final score: sum over all final states
273
+ score = jax.scipy.special.logsumexp(jnp.concatenate([final_M, final_I]))
274
+
275
+ return score
276
+
277
+ def compute_posteriors(
278
+ self,
279
+ sequence: Float[Array, "seq_len alphabet_size"],
280
+ ) -> Float[Array, "seq_len profile_length 3"]:
281
+ """Compute state posteriors (simplified).
282
+
283
+ Returns soft alignment between sequence and profile positions.
284
+
285
+ Args:
286
+ sequence: One-hot encoded sequence.
287
+
288
+ Returns:
289
+ Posterior probabilities for M/I/D states at each position.
290
+ """
291
+ seq_len = sequence.shape[0]
292
+ log_match = self.get_match_emissions()
293
+
294
+ # Simplified: just compute match scores as posteriors
295
+ match_scores = jnp.einsum("sa,pa->sp", sequence, jnp.exp(log_match))
296
+
297
+ # Normalize across profile positions for each sequence position
298
+ posteriors = jax.nn.softmax(match_scores / self._temperature, axis=1)
299
+
300
+ # Expand to include I/D placeholder dimensions
301
+ # Shape: (seq_len, profile_length, 3) where dim 2 is [M, I, D]
302
+ full_posteriors = jnp.zeros((seq_len, self.profile_length, 3))
303
+ full_posteriors = full_posteriors.at[:, :, 0].set(posteriors)
304
+
305
+ return full_posteriors
306
+
307
+ def apply(
308
+ self,
309
+ data: PyTree,
310
+ state: PyTree,
311
+ metadata: dict[str, Any] | None,
312
+ random_params: Any = None,
313
+ stats: dict[str, Any] | None = None,
314
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
315
+ """Apply profile HMM search to sequence.
316
+
317
+ Args:
318
+ data: Dictionary containing:
319
+ - "sequence": One-hot encoded sequence (seq_len, alphabet_size)
320
+ state: Element state (passed through unchanged)
321
+ metadata: Element metadata (passed through unchanged)
322
+ random_params: Not used (deterministic operator)
323
+ stats: Not used
324
+
325
+ Returns:
326
+ Tuple of (transformed_data, state, metadata):
327
+ - transformed_data contains:
328
+
329
+ - "sequence": Original sequence
330
+ - "score": Profile alignment score
331
+ - "state_posteriors": Soft state assignments
332
+ - state is passed through unchanged
333
+ - metadata is passed through unchanged
334
+ """
335
+ sequence = data["sequence"]
336
+
337
+ # Compute alignment score
338
+ score = self.score_sequence(sequence)
339
+
340
+ # Compute state posteriors
341
+ state_posteriors = self.compute_posteriors(sequence)
342
+
343
+ # Build output data
344
+ transformed_data = {
345
+ "sequence": sequence,
346
+ "score": score,
347
+ "state_posteriors": state_posteriors,
348
+ }
349
+
350
+ return transformed_data, state, metadata
@@ -0,0 +1,127 @@
1
+ """Scoring matrices for sequence alignment.
2
+
3
+ This module provides pre-defined scoring matrices and utilities for
4
+ creating custom scoring matrices for DNA, RNA, and protein alignment.
5
+ """
6
+
7
+ import functools
8
+ import logging
9
+ from typing import NamedTuple
10
+
11
+ import jax.numpy as jnp
12
+ from jaxtyping import Array, Float
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ScoringMatrix(NamedTuple):
18
+ """Scoring matrix with metadata.
19
+
20
+ Attributes:
21
+ matrix: The scoring matrix array.
22
+ alphabet: The alphabet string (e.g., "ACGT" for DNA).
23
+ name: Optional name for the matrix.
24
+ """
25
+
26
+ matrix: Float[Array, "alphabet alphabet"]
27
+ alphabet: str
28
+ name: str = ""
29
+
30
+
31
+ def create_dna_scoring_matrix(
32
+ match: float = 2.0,
33
+ mismatch: float = -1.0,
34
+ ) -> Float[Array, "4 4"]:
35
+ """Create a simple DNA scoring matrix.
36
+
37
+ Args:
38
+ match: Score for matching nucleotides (diagonal).
39
+ mismatch: Score for mismatching nucleotides (off-diagonal).
40
+
41
+ Returns:
42
+ 4x4 scoring matrix for DNA (A, C, G, T order).
43
+ """
44
+ # Create identity matrix scaled by match score
45
+ identity = jnp.eye(4) * match
46
+ # Create off-diagonal with mismatch score
47
+ off_diag = (jnp.ones((4, 4)) - jnp.eye(4)) * mismatch
48
+ return identity + off_diag
49
+
50
+
51
+ def create_rna_scoring_matrix(
52
+ match: float = 2.0,
53
+ mismatch: float = -1.0,
54
+ ) -> Float[Array, "4 4"]:
55
+ """Create a simple RNA scoring matrix.
56
+
57
+ Args:
58
+ match: Score for matching nucleotides (diagonal).
59
+ mismatch: Score for mismatching nucleotides (off-diagonal).
60
+
61
+ Returns:
62
+ 4x4 scoring matrix for RNA (A, C, G, U order).
63
+ """
64
+ # RNA uses same simple scoring as DNA
65
+ return create_dna_scoring_matrix(match, mismatch)
66
+
67
+
68
+ @functools.cache
69
+ def get_dna_simple() -> Float[Array, "4 4"]:
70
+ """Get pre-defined DNA scoring matrix (simple match/mismatch).
71
+
72
+ Returns:
73
+ 4x4 scoring matrix with match=2.0, mismatch=-1.0.
74
+ """
75
+ return create_dna_scoring_matrix(match=2.0, mismatch=-1.0)
76
+
77
+
78
+ @functools.cache
79
+ def get_rna_simple() -> Float[Array, "4 4"]:
80
+ """Get pre-defined RNA scoring matrix (simple match/mismatch).
81
+
82
+ Returns:
83
+ 4x4 scoring matrix with match=2.0, mismatch=-1.0.
84
+ """
85
+ return create_rna_scoring_matrix(match=2.0, mismatch=-1.0)
86
+
87
+
88
+ # BLOSUM62 scoring matrix for proteins (20 amino acids)
89
+ # Standard BLOSUM62 matrix values
90
+ # Amino acid order: A, R, N, D, C, Q, E, G, H, I, L, K, M, F, P, S, T, W, Y, V
91
+ _BLOSUM62_VALUES = [
92
+ # A R N D C Q E G H I L K M F P S T W Y V
93
+ [4, -1, -2, -2, 0, -1, -1, 0, -2, -1, -1, -1, -1, -2, -1, 1, 0, -3, -2, 0], # A
94
+ [-1, 5, 0, -2, -3, 1, 0, -2, 0, -3, -2, 2, -1, -3, -2, -1, -1, -3, -2, -3], # R
95
+ [-2, 0, 6, 1, -3, 0, 0, 0, 1, -3, -3, 0, -2, -3, -2, 1, 0, -4, -2, -3], # N
96
+ [-2, -2, 1, 6, -3, 0, 2, -1, -1, -3, -4, -1, -3, -3, -1, 0, -1, -4, -3, -3], # D
97
+ [0, -3, -3, -3, 9, -3, -4, -3, -3, -1, -1, -3, -1, -2, -3, -1, -1, -2, -2, -1], # C
98
+ [-1, 1, 0, 0, -3, 5, 2, -2, 0, -3, -2, 1, 0, -3, -1, 0, -1, -2, -1, -2], # Q
99
+ [-1, 0, 0, 2, -4, 2, 5, -2, 0, -3, -3, 1, -2, -3, -1, 0, -1, -3, -2, -2], # E
100
+ [0, -2, 0, -1, -3, -2, -2, 6, -2, -4, -4, -2, -3, -3, -2, 0, -2, -2, -3, -3], # G
101
+ [-2, 0, 1, -1, -3, 0, 0, -2, 8, -3, -3, -1, -2, -1, -2, -1, -2, -2, 2, -3], # H
102
+ [-1, -3, -3, -3, -1, -3, -3, -4, -3, 4, 2, -3, 1, 0, -3, -2, -1, -3, -1, 3], # I
103
+ [-1, -2, -3, -4, -1, -2, -3, -4, -3, 2, 4, -2, 2, 0, -3, -2, -1, -2, -1, 1], # L
104
+ [-1, 2, 0, -1, -3, 1, 1, -2, -1, -3, -2, 5, -1, -3, -1, 0, -1, -3, -2, -2], # K
105
+ [-1, -1, -2, -3, -1, 0, -2, -3, -2, 1, 2, -1, 5, 0, -2, -1, -1, -1, -1, 1], # M
106
+ [-2, -3, -3, -3, -2, -3, -3, -3, -1, 0, 0, -3, 0, 6, -4, -2, -2, 1, 3, -1], # F
107
+ [-1, -2, -2, -1, -3, -1, -1, -2, -2, -3, -3, -1, -2, -4, 7, -1, -1, -4, -3, -2], # P
108
+ [1, -1, 1, 0, -1, 0, 0, 0, -1, -2, -2, 0, -1, -2, -1, 4, 1, -3, -2, -2], # S
109
+ [0, -1, 0, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, -2, -1, 1, 5, -2, -2, 0], # T
110
+ [-3, -3, -4, -4, -2, -2, -3, -2, -2, -3, -2, -3, -1, 1, -4, -3, -2, 11, 2, -3], # W
111
+ [-2, -2, -2, -3, -2, -1, -2, -3, 2, -1, -1, -2, -1, 3, -3, -2, -2, 2, 7, -1], # Y
112
+ [0, -3, -3, -3, -1, -2, -2, -3, -3, 3, 1, -2, 1, -1, -2, -2, 0, -3, -1, 4], # V
113
+ ]
114
+
115
+
116
+ @functools.cache
117
+ def get_blosum62() -> Float[Array, "20 20"]:
118
+ """Get BLOSUM62 scoring matrix for protein alignment.
119
+
120
+ Returns:
121
+ 20x20 scoring matrix for 20 standard amino acids.
122
+ """
123
+ return jnp.array(_BLOSUM62_VALUES, dtype=jnp.float32)
124
+
125
+
126
+ # Amino acid alphabet for BLOSUM62
127
+ PROTEIN_ALPHABET = "ARNDCQEGHILKMFPSTWYV"