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,509 @@
1
+ """Differentiable secondary structure prediction (PyDSSP-style).
2
+
3
+ This module implements a JAX/Flax NNX version of the DSSP algorithm for
4
+ assigning secondary structure to protein backbone atoms. The key innovation
5
+ is a continuous hydrogen bond matrix that enables gradient-based optimization.
6
+
7
+ The algorithm computes hydrogen bond energies using the Kabsch-Sander formula,
8
+ then applies a smooth transformation to create a differentiable H-bond matrix.
9
+ Secondary structure is assigned based on characteristic H-bond patterns.
10
+
11
+ Reference:
12
+ Kabsch & Sander (1983). Dictionary of protein secondary structure:
13
+ pattern recognition of hydrogen-bonded and geometrical features.
14
+ Biopolymers 22, 2577-2637.
15
+
16
+ Minami (2023). PyDSSP: A simplified implementation of DSSP algorithm
17
+ for PyTorch and NumPy. https://github.com/ShintaroMinami/PyDSSP
18
+ """
19
+
20
+ import logging
21
+ from dataclasses import dataclass
22
+ from typing import Any
23
+
24
+ import jax.numpy as jnp
25
+ from datarax.core.config import OperatorConfig
26
+ from datarax.core.operator import OperatorModule
27
+ from flax import nnx
28
+ from jaxtyping import Array, Float
29
+
30
+ from diffbio.core import soft_ops
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # DSSP constants from Kabsch & Sander (1983)
35
+ CONST_Q1Q2 = 0.084 # Partial charges (e units)
36
+ CONST_F = 332.0 # Conversion factor to kcal/mol
37
+ DEFAULT_CUTOFF = -0.5 # H-bond energy threshold (kcal/mol)
38
+ DEFAULT_MARGIN = 1.0 # Smoothing margin for continuous H-bond map
39
+
40
+ # Atom indices in the coordinate array
41
+ ATOM_N = 0
42
+ ATOM_CA = 1
43
+ ATOM_C = 2
44
+ ATOM_O = 3
45
+
46
+ # Secondary structure class indices
47
+ SS_LOOP = 0 # '-' (coil/loop)
48
+ SS_HELIX = 1 # 'H' (alpha-helix)
49
+ SS_STRAND = 2 # 'E' (beta-strand)
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class _SecondaryStructureCoreConfig:
54
+ """Core DSSP thresholding configuration."""
55
+
56
+ margin: float = DEFAULT_MARGIN
57
+ cutoff: float = DEFAULT_CUTOFF
58
+ min_helix_length: int = 4
59
+ temperature: float = 1.0
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class _SecondaryStructureConstraintConfig:
64
+ """Optional geometric regularization configuration."""
65
+
66
+ use_bond_length_constraint: bool = False
67
+ use_bond_angle_constraint: bool = False
68
+ bond_length_weight: float = 0.1
69
+ bond_angle_weight: float = 0.1
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class SecondaryStructureConfig(
74
+ _SecondaryStructureCoreConfig,
75
+ _SecondaryStructureConstraintConfig,
76
+ OperatorConfig,
77
+ ):
78
+ """Configuration for DifferentiableSecondaryStructure."""
79
+
80
+ def __post_init__(self) -> None:
81
+ """Validate the secondary-structure configuration."""
82
+ super().__post_init__()
83
+
84
+ if self.margin <= 0.0:
85
+ raise ValueError("margin must be positive.")
86
+ if self.min_helix_length <= 0:
87
+ raise ValueError("min_helix_length must be positive.")
88
+ if self.temperature <= 0.0:
89
+ raise ValueError("temperature must be positive.")
90
+ if self.bond_length_weight < 0.0:
91
+ raise ValueError("bond_length_weight must be non-negative.")
92
+ if self.bond_angle_weight < 0.0:
93
+ raise ValueError("bond_angle_weight must be non-negative.")
94
+
95
+
96
+ def compute_hydrogen_position(
97
+ n_pos: Float[Array, "... 3"],
98
+ ca_pos: Float[Array, "... 3"],
99
+ c_prev_pos: Float[Array, "... 3"],
100
+ ) -> Float[Array, "... 3"]:
101
+ """Compute hydrogen atom position from backbone atoms.
102
+
103
+ The amide hydrogen is placed along the N-H bond direction, which is
104
+ approximately opposite to the bisector of CA-N and C_prev-N vectors.
105
+
106
+ Args:
107
+ n_pos: Nitrogen atom positions.
108
+ ca_pos: Alpha carbon positions.
109
+ c_prev_pos: Carbonyl carbon from previous residue.
110
+
111
+ Returns:
112
+ Estimated hydrogen atom positions.
113
+ """
114
+ # Vectors from N to neighboring atoms
115
+ vec_n_ca = ca_pos - n_pos
116
+ vec_n_c = c_prev_pos - n_pos
117
+
118
+ # Normalize
119
+ vec_n_ca = vec_n_ca / (jnp.linalg.norm(vec_n_ca, axis=-1, keepdims=True) + 1e-8)
120
+ vec_n_c = vec_n_c / (jnp.linalg.norm(vec_n_c, axis=-1, keepdims=True) + 1e-8)
121
+
122
+ # H is opposite to the average direction (bisector)
123
+ h_direction = -(vec_n_ca + vec_n_c)
124
+ h_direction = h_direction / (jnp.linalg.norm(h_direction, axis=-1, keepdims=True) + 1e-8)
125
+
126
+ # Standard N-H bond length is ~1.0 Angstrom
127
+ h_pos = n_pos + h_direction * 1.0
128
+
129
+ return h_pos
130
+
131
+
132
+ class DifferentiableSecondaryStructure(OperatorModule):
133
+ """Differentiable secondary structure prediction using DSSP algorithm.
134
+
135
+ This operator computes secondary structure assignments for protein
136
+ backbone atoms using a differentiable version of the DSSP algorithm.
137
+ The key innovation is a continuous hydrogen bond matrix that enables
138
+ gradient flow through the secondary structure prediction.
139
+
140
+ The algorithm:
141
+ 1. Compute hydrogen bond energies using Kabsch-Sander electrostatic formula
142
+ 2. Apply smooth transformation to create continuous H-bond matrix in [0,1]
143
+ 3. Detect helix patterns (i→i+4 H-bonds) and strand patterns
144
+ 4. Output soft secondary structure assignments
145
+
146
+ Input data structure:
147
+ - coordinates: Float[Array, "batch length 4 3"] - Backbone atoms (N, CA, C, O)
148
+
149
+ Output data structure (adds):
150
+ - ss_onehot: Float[Array, "batch length 3"] - Soft SS probabilities
151
+ - hbond_map: Float[Array, "batch length length"] - Continuous H-bond matrix
152
+ - ss_indices: Int[Array, "batch length"] - Hard SS assignments (0=loop, 1=helix, 2=strand)
153
+
154
+ Example:
155
+ ```python
156
+ config = SecondaryStructureConfig(margin=1.0, cutoff=-0.5)
157
+ predictor = DifferentiableSecondaryStructure(config, rngs=nnx.Rngs(42))
158
+ coords = jax.random.uniform(key, (1, 50, 4, 3)) * 10 # 50 residues
159
+ result, _, _ = predictor.apply({"coordinates": coords}, {}, None)
160
+ ss_probs = result["ss_onehot"] # (1, 50, 3)
161
+ ```
162
+ """
163
+
164
+ def __init__(
165
+ self,
166
+ config: SecondaryStructureConfig,
167
+ *,
168
+ rngs: nnx.Rngs,
169
+ name: str | None = None,
170
+ ):
171
+ """Initialize the secondary structure predictor.
172
+
173
+ Args:
174
+ config: Configuration with DSSP parameters.
175
+ rngs: Random number generators.
176
+ name: Optional name for the operator.
177
+ """
178
+ super().__init__(config, rngs=rngs, name=name)
179
+ self.config: SecondaryStructureConfig = config
180
+
181
+ def compute_hbond_energy(
182
+ self,
183
+ coords: Float[Array, "batch length 4 3"],
184
+ ) -> Float[Array, "batch length length"]:
185
+ """Compute hydrogen bond energy matrix.
186
+
187
+ Uses the Kabsch-Sander electrostatic energy formula:
188
+ E = q1*q2 * f * (1/r_ON + 1/r_CH - 1/r_OH - 1/r_CN)
189
+
190
+ where r_XY is the distance between atoms X and Y.
191
+
192
+ Donor: N-H from residue i
193
+ Acceptor: C=O from residue j
194
+
195
+ Args:
196
+ coords: Backbone coordinates (batch, length, 4, 3).
197
+ Atom order: N, CA, C, O
198
+
199
+ Returns:
200
+ Energy matrix (batch, length, length) in kcal/mol.
201
+ E[b, i, j] = energy of H-bond from donor i to acceptor j.
202
+ """
203
+ batch, length, _, _ = coords.shape
204
+
205
+ # Extract atom positions
206
+ n_pos = coords[:, :, ATOM_N, :] # (batch, length, 3)
207
+ ca_pos = coords[:, :, ATOM_CA, :]
208
+ c_pos = coords[:, :, ATOM_C, :]
209
+ o_pos = coords[:, :, ATOM_O, :]
210
+
211
+ # Compute hydrogen positions (shifted by 1 for C from previous residue)
212
+ # For first residue, use self C as approximation
213
+ c_prev = jnp.concatenate([c_pos[:, :1, :], c_pos[:, :-1, :]], axis=1)
214
+ h_pos = compute_hydrogen_position(n_pos, ca_pos, c_prev)
215
+
216
+ # Expand for pairwise computation
217
+ # Donors: N, H from residue i
218
+ # Acceptors: C, O from residue j
219
+ n_i = n_pos[:, :, None, :] # (batch, length, 1, 3)
220
+ h_i = h_pos[:, :, None, :]
221
+ c_j = c_pos[:, None, :, :] # (batch, 1, length, 3)
222
+ o_j = o_pos[:, None, :, :]
223
+
224
+ # Compute distances with numerical stability
225
+ def safe_distance(pos1, pos2, min_dist=0.1):
226
+ diff = pos1 - pos2
227
+ dist = jnp.sqrt(jnp.sum(diff**2, axis=-1) + 1e-10)
228
+ return jnp.maximum(dist, min_dist)
229
+
230
+ d_on = safe_distance(o_j, n_i) # O(acceptor) to N(donor)
231
+ d_ch = safe_distance(c_j, h_i) # C(acceptor) to H(donor)
232
+ d_oh = safe_distance(o_j, h_i) # O(acceptor) to H(donor)
233
+ d_cn = safe_distance(c_j, n_i) # C(acceptor) to N(donor)
234
+
235
+ # Kabsch-Sander energy formula
236
+ energy = CONST_Q1Q2 * CONST_F * (1.0 / d_on + 1.0 / d_ch - 1.0 / d_oh - 1.0 / d_cn)
237
+
238
+ return energy
239
+
240
+ def compute_hbond_map(
241
+ self,
242
+ coords: Float[Array, "batch length 4 3"],
243
+ ) -> Float[Array, "batch length length"]:
244
+ """Compute continuous hydrogen bond matrix.
245
+
246
+ Transforms the energy matrix into a continuous [0,1] matrix using
247
+ a smooth sigmoid-like function based on sine:
248
+ HbondMat(i,j) = (1 + sin((cutoff - E - margin) / margin * pi/2)) / 2
249
+
250
+ This allows gradients to flow through the H-bond detection.
251
+
252
+ Args:
253
+ coords: Backbone coordinates (batch, length, 4, 3).
254
+
255
+ Returns:
256
+ Continuous H-bond matrix (batch, length, length) in [0, 1].
257
+ """
258
+ energy = self.compute_hbond_energy(coords)
259
+
260
+ margin = self.config.margin
261
+ cutoff = self.config.cutoff
262
+
263
+ # Smooth transformation: maps energy to [0, 1]
264
+ # More negative energy (stronger H-bond) -> higher value
265
+ x = (cutoff - energy - margin) / margin
266
+
267
+ # Clamp to [-1, 1] for sin input to stay in valid range
268
+ x = jnp.clip(x, -1.0, 1.0)
269
+
270
+ # Sine-based sigmoid: smooth transition in [0, 1]
271
+ hbond_map = (1.0 + jnp.sin(x * jnp.pi / 2)) / 2.0
272
+
273
+ return hbond_map
274
+
275
+ def detect_helix_pattern(
276
+ self,
277
+ hbond_map: Float[Array, "batch length length"],
278
+ ) -> Float[Array, "batch length"]:
279
+ """Detect alpha-helix pattern (i→i+4 hydrogen bonds).
280
+
281
+ Alpha-helices are characterized by H-bonds from residue i (donor)
282
+ to residue i-4 (acceptor), creating i→i+4 backbone H-bonds.
283
+
284
+ Args:
285
+ hbond_map: Continuous H-bond matrix.
286
+
287
+ Returns:
288
+ Soft helix assignment for each residue.
289
+ """
290
+ batch, length, _ = hbond_map.shape
291
+
292
+ # Extract i→i+4 diagonal (donor i to acceptor i-4)
293
+ # This means hbond_map[i, i-4] should be high
294
+ helix_score = jnp.zeros((batch, length))
295
+
296
+ # For residues 4 and beyond, check if they donate to i-4
297
+ # and residue i+4 donates to them
298
+ for offset in [3, 4]: # Check both i→i+3 (3-10 helix) and i→i+4 (alpha helix)
299
+ # Create shifted versions to extract diagonal
300
+ if offset < length:
301
+ # Score for being in a helix: both i→i-offset and i+offset→i should be H-bonded
302
+ # Simplified: just check i→i-offset pattern
303
+ indices = jnp.arange(length)
304
+ donor_indices = indices
305
+ acceptor_indices = indices - offset
306
+
307
+ # Mask for valid indices
308
+ valid_mask = acceptor_indices >= 0
309
+
310
+ # Get H-bond scores for valid pairs
311
+ scores = jnp.where(
312
+ valid_mask,
313
+ hbond_map[:, donor_indices, jnp.maximum(acceptor_indices, 0)],
314
+ 0.0,
315
+ )
316
+
317
+ helix_score = helix_score + scores
318
+
319
+ # Normalize and apply temperature
320
+ helix_score = helix_score / 2.0 # Average of two patterns
321
+ helix_score = soft_ops.clip(helix_score, 0.0, 1.0, softness=0.1)
322
+
323
+ return helix_score
324
+
325
+ def detect_strand_pattern(
326
+ self,
327
+ hbond_map: Float[Array, "batch length length"],
328
+ ) -> Float[Array, "batch length"]:
329
+ """Detect beta-strand pattern (parallel/antiparallel H-bonds).
330
+
331
+ Beta-strands are characterized by H-bonds between distant residues
332
+ forming ladder-like patterns (parallel or antiparallel).
333
+
334
+ Args:
335
+ hbond_map: Continuous H-bond matrix.
336
+
337
+ Returns:
338
+ Soft strand assignment for each residue.
339
+ """
340
+ batch, length, _ = hbond_map.shape
341
+
342
+ # For strands, we look for H-bonds to non-local residues (|i-j| > 4)
343
+ # Create distance mask
344
+ i_idx = jnp.arange(length)[:, None]
345
+ j_idx = jnp.arange(length)[None, :]
346
+ distance_mask = jnp.abs(i_idx - j_idx) > 4
347
+
348
+ # Mask H-bond map to only consider non-local bonds
349
+ masked_hbond = hbond_map * distance_mask[None, :, :]
350
+
351
+ # Score for each residue: max of incoming and outgoing non-local H-bonds
352
+ incoming = soft_ops.max(masked_hbond, axis=1, softness=0.1)
353
+ outgoing = soft_ops.max(masked_hbond, axis=2, softness=0.1)
354
+
355
+ strand_score = jnp.maximum(incoming, outgoing)
356
+
357
+ return strand_score
358
+
359
+ def assign_secondary_structure(
360
+ self,
361
+ hbond_map: Float[Array, "batch length length"],
362
+ ) -> Float[Array, "batch length 3"]:
363
+ """Assign secondary structure based on H-bond patterns.
364
+
365
+ Combines helix and strand detection into soft assignments.
366
+
367
+ Args:
368
+ hbond_map: Continuous H-bond matrix.
369
+
370
+ Returns:
371
+ One-hot encoded SS assignments (batch, length, 3).
372
+ Classes: 0=loop, 1=helix, 2=strand
373
+ """
374
+ helix_score = self.detect_helix_pattern(hbond_map)
375
+ strand_score = self.detect_strand_pattern(hbond_map)
376
+
377
+ # Stack scores: [loop, helix, strand]
378
+ # Loop score is complement of max(helix, strand)
379
+ loop_score = 1.0 - jnp.maximum(helix_score, strand_score)
380
+
381
+ scores = jnp.stack([loop_score, helix_score, strand_score], axis=-1)
382
+
383
+ # Apply softmax with temperature for soft assignments
384
+ temp = self.config.temperature
385
+ ss_probs = nnx.softmax(scores / temp, axis=-1)
386
+
387
+ return ss_probs
388
+
389
+ def apply(
390
+ self,
391
+ data: dict[str, Any],
392
+ state: dict[str, Any],
393
+ metadata: dict[str, Any] | None,
394
+ random_params: Any = None, # noqa: ARG002
395
+ stats: dict[str, Any] | None = None, # noqa: ARG002
396
+ ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]:
397
+ """Apply secondary structure prediction.
398
+
399
+ Args:
400
+ data: Input data containing:
401
+ - coordinates: Float[Array, "batch length 4 3"]
402
+ state: Element state (passed through).
403
+ metadata: Element metadata (passed through).
404
+ random_params: Random parameters (unused).
405
+ stats: Optional statistics (unused).
406
+
407
+ Returns:
408
+ Tuple of (output_data, state, metadata).
409
+ """
410
+ coords = data["coordinates"]
411
+
412
+ # Compute H-bond matrix
413
+ hbond_map = self.compute_hbond_map(coords)
414
+
415
+ # Assign secondary structure
416
+ ss_onehot = self.assign_secondary_structure(hbond_map)
417
+
418
+ # Hard assignments for convenience
419
+ ss_indices = jnp.argmax(ss_onehot, axis=-1)
420
+
421
+ # Build output
422
+ output_data = {
423
+ **data,
424
+ "hbond_map": hbond_map,
425
+ "ss_onehot": ss_onehot,
426
+ "ss_indices": ss_indices,
427
+ }
428
+
429
+ # Optionally compute artifex backbone constraint losses
430
+ constraint_loss = _compute_backbone_constraints(coords, self.config)
431
+ if constraint_loss is not None:
432
+ output_data["backbone_constraint_loss"] = constraint_loss
433
+
434
+ return output_data, state, metadata
435
+
436
+
437
+ def _compute_backbone_constraints(
438
+ coords: Float[Array, "batch length 4 3"],
439
+ config: SecondaryStructureConfig,
440
+ ) -> Array | None:
441
+ """Compute artifex backbone constraint losses if enabled.
442
+
443
+ Uses artifex BondLengthExtension and BondAngleExtension to compute
444
+ regularisation losses that penalise deviation from ideal protein
445
+ backbone geometry.
446
+
447
+ Args:
448
+ coords: Backbone coordinates (batch, length, 4, 3).
449
+ config: Config with constraint flags and weights.
450
+
451
+ Returns:
452
+ Scalar constraint loss, or None if constraints are disabled.
453
+ """
454
+ if not config.use_bond_length_constraint and not config.use_bond_angle_constraint:
455
+ return None
456
+
457
+ from artifex.generative_models.extensions.protein.backbone import ( # noqa: PLC0415
458
+ BondAngleExtension,
459
+ BondLengthExtension,
460
+ )
461
+ from artifex.generative_models.extensions.protein.backbone import ( # noqa: PLC0415
462
+ ProteinExtensionConfig,
463
+ )
464
+
465
+ batch_data = {"coordinates": coords}
466
+ total_loss = jnp.float32(0.0)
467
+
468
+ if config.use_bond_length_constraint:
469
+ ext_config = ProteinExtensionConfig()
470
+ ext = BondLengthExtension(ext_config, rngs=nnx.Rngs(0))
471
+ bl_loss = ext.loss_fn(batch_data, None)
472
+ total_loss = total_loss + config.bond_length_weight * bl_loss
473
+
474
+ if config.use_bond_angle_constraint:
475
+ ext_config = ProteinExtensionConfig()
476
+ ext = BondAngleExtension(ext_config, rngs=nnx.Rngs(0))
477
+ ba_loss = ext.loss_fn(batch_data, None)
478
+ total_loss = total_loss + config.bond_angle_weight * ba_loss
479
+
480
+ return total_loss
481
+
482
+
483
+ def create_secondary_structure_predictor(
484
+ margin: float = DEFAULT_MARGIN,
485
+ cutoff: float = DEFAULT_CUTOFF,
486
+ min_helix_length: int = 4,
487
+ temperature: float = 1.0,
488
+ seed: int = 42,
489
+ ) -> DifferentiableSecondaryStructure:
490
+ """Factory function to create a secondary structure predictor.
491
+
492
+ Args:
493
+ margin: Smoothing margin for H-bond matrix. Default 1.0.
494
+ cutoff: H-bond energy threshold in kcal/mol. Default -0.5.
495
+ min_helix_length: Minimum residues for helix. Default 4.
496
+ temperature: Softmax temperature. Default 1.0.
497
+ seed: Random seed. Default 42.
498
+
499
+ Returns:
500
+ Configured DifferentiableSecondaryStructure instance.
501
+ """
502
+ config = SecondaryStructureConfig(
503
+ margin=margin,
504
+ cutoff=cutoff,
505
+ min_helix_length=min_helix_length,
506
+ temperature=temperature,
507
+ )
508
+ rngs = nnx.Rngs(seed)
509
+ return DifferentiableSecondaryStructure(config, rngs=rngs)
@@ -0,0 +1,128 @@
1
+ """Differentiable quality filter operator for bioinformatics sequences.
2
+
3
+ This module provides a soft quality filtering operator that down-weights
4
+ low-quality positions in sequences using a differentiable sigmoid function.
5
+ """
6
+
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+ from datarax.core.config import OperatorConfig
12
+ from datarax.core.operator import OperatorModule
13
+ from flax import nnx
14
+ from jaxtyping import PyTree
15
+
16
+ from diffbio.constants import PHRED_QUALITY_THRESHOLD
17
+ from diffbio.core import soft_ops
18
+ from diffbio.utils.nn_utils import init_learnable_param
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class QualityFilterConfig(OperatorConfig):
25
+ """Configuration for DifferentiableQualityFilter.
26
+
27
+ Attributes:
28
+ initial_threshold: Initial Phred quality score threshold.
29
+ Positions with quality below this are down-weighted.
30
+ Default is 20.0 (1% error rate).
31
+ """
32
+
33
+ initial_threshold: float = PHRED_QUALITY_THRESHOLD
34
+
35
+
36
+ class DifferentiableQualityFilter(OperatorModule):
37
+ """Differentiable quality filter for DNA/RNA sequences.
38
+
39
+ This operator applies soft quality filtering using a sigmoid function
40
+ to weight sequence positions by their quality scores. High-quality
41
+ positions (above threshold) pass through with high weight, while
42
+ low-quality positions are down-weighted.
43
+
44
+ The threshold is a learnable parameter that can be optimized
45
+ end-to-end with the rest of the pipeline.
46
+
47
+ Formula:
48
+ retention_weight = sigmoid(quality_score - threshold)
49
+ filtered_sequence = sequence * retention_weight
50
+
51
+ Args:
52
+ config: QualityFilterConfig with initial threshold
53
+ rngs: Flax NNX random number generators
54
+
55
+ Example:
56
+ ```python
57
+ config = QualityFilterConfig(initial_threshold=20.0)
58
+ filter_op = DifferentiableQualityFilter(config, rngs=nnx.Rngs(42))
59
+ data = {"sequence": encoded_seq, "quality_scores": quality}
60
+ filtered_data, state, meta = filter_op.apply(data, {}, None, None)
61
+ ```
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ config: QualityFilterConfig,
67
+ *,
68
+ rngs: nnx.Rngs | None = None,
69
+ name: str | None = None,
70
+ ):
71
+ """Initialize the quality filter with learnable threshold.
72
+
73
+ Args:
74
+ config: Quality filter configuration
75
+ rngs: Random number generators (optional for deterministic ops)
76
+ name: Optional operator name
77
+ """
78
+ super().__init__(config, rngs=rngs, name=name)
79
+
80
+ # Learnable threshold parameter
81
+ self.threshold = init_learnable_param(config.initial_threshold)
82
+
83
+ def apply(
84
+ self,
85
+ data: PyTree,
86
+ state: PyTree,
87
+ metadata: dict[str, Any] | None,
88
+ random_params: Any = None,
89
+ stats: dict[str, Any] | None = None,
90
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
91
+ """Apply soft quality filtering to sequence data.
92
+
93
+ This method applies a differentiable quality filter that weights
94
+ each position by sigmoid(quality - threshold). High quality
95
+ positions retain most of their value, while low quality positions
96
+ are down-weighted.
97
+
98
+ Args:
99
+ data: Dictionary containing:
100
+ - "sequence": One-hot encoded sequence (length, alphabet_size)
101
+ - "quality_scores": Phred quality scores (length,)
102
+ state: Element state (passed through unchanged)
103
+ metadata: Element metadata (passed through unchanged)
104
+ random_params: Not used (deterministic operator)
105
+ stats: Not used
106
+
107
+ Returns:
108
+ Tuple of (transformed_data, state, metadata):
109
+ - transformed_data contains weighted sequence and original quality
110
+ - state is passed through unchanged
111
+ - metadata is passed through unchanged
112
+ """
113
+ sequence = data["sequence"]
114
+ quality_scores = data["quality_scores"]
115
+
116
+ # Soft comparison: high quality -> weight ~1, low quality -> weight ~0
117
+ retention_weights = soft_ops.greater(quality_scores, self.threshold[...], softness=1.0)
118
+
119
+ # Apply weights to sequence (broadcast over alphabet dimension)
120
+ weighted_sequence = sequence * retention_weights[:, None]
121
+
122
+ # Build output data (preserve quality scores for downstream use)
123
+ transformed_data = {
124
+ "sequence": weighted_sequence,
125
+ "quality_scores": quality_scores,
126
+ }
127
+
128
+ return transformed_data, state, metadata
@@ -0,0 +1,35 @@
1
+ """RNA secondary structure prediction operators for DiffBio.
2
+
3
+ This module provides differentiable operators for RNA secondary structure
4
+ prediction, following the McCaskill partition function algorithm for
5
+ computing base pair probabilities.
6
+
7
+ Operators:
8
+ DifferentiableRNAFold: McCaskill-style RNA folding with base pair probs
9
+
10
+ Factory Functions:
11
+ create_rna_fold_predictor: Create RNA fold predictor with defaults
12
+
13
+ References:
14
+ McCaskill (1990). The equilibrium partition function and base pair
15
+ binding probabilities for RNA secondary structure.
16
+
17
+ Matthies et al. (2024). Differentiable partition function calculation
18
+ for RNA. Nucleic Acids Research.
19
+ """
20
+
21
+ from diffbio.operators.rna_structure.rna_folding import (
22
+ DifferentiableRNAFold,
23
+ RNAFoldConfig,
24
+ compute_base_pair_probabilities,
25
+ compute_pair_energy_matrix,
26
+ create_rna_fold_predictor,
27
+ )
28
+
29
+ __all__ = [
30
+ "DifferentiableRNAFold",
31
+ "RNAFoldConfig",
32
+ "create_rna_fold_predictor",
33
+ "compute_pair_energy_matrix",
34
+ "compute_base_pair_probabilities",
35
+ ]