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,15 @@
1
+ """Type aliases for soft differentiable operations.
2
+
3
+ SoftBool represents a probability in [0, 1] -- a soft relaxation of a
4
+ boolean value. SoftIndex represents a probability distribution over
5
+ discrete indices -- a soft relaxation of an integer index.
6
+ """
7
+
8
+ from jax import Array
9
+ from jaxtyping import Float
10
+
11
+ SoftBool = Float[Array, "..."]
12
+ """Soft boolean: probability in [0, 1]."""
13
+
14
+ SoftIndex = Float[Array, "..."]
15
+ """Soft index: probabilities summing to 1 along the last axis."""
@@ -0,0 +1,342 @@
1
+ """Internal utility functions for soft_ops.
2
+
3
+ These are shared helpers used across the soft_ops submodules.
4
+ Not part of the public API.
5
+ """
6
+
7
+ from typing import Literal, overload
8
+
9
+ import jax
10
+ import jax.numpy as jnp
11
+ from jax import Array
12
+
13
+
14
+ def validate_softness(softness: float | Array) -> None:
15
+ """Raise ``ValueError`` if softness is not positive.
16
+
17
+ Validation is skipped inside JAX-traced contexts (jit, grad, vmap)
18
+ since concrete values are not available during tracing.
19
+
20
+ Args:
21
+ softness: The softness (temperature) parameter. Must be > 0.
22
+ """
23
+ if isinstance(softness, jax.core.Tracer):
24
+ return
25
+ if float(softness) <= 0:
26
+ msg = f"softness must be positive, got {softness}"
27
+ raise ValueError(msg)
28
+
29
+
30
+ def ensure_float(x: float | Array | jnp.ndarray) -> jnp.ndarray:
31
+ """Cast to default float dtype if not already floating point.
32
+
33
+ Args:
34
+ x: Input array or scalar.
35
+
36
+ Returns:
37
+ Array with floating-point dtype.
38
+ """
39
+ x = jnp.asarray(x)
40
+ if jnp.issubdtype(x.dtype, jnp.floating):
41
+ return x
42
+ return x.astype(jnp.result_type(float))
43
+
44
+
45
+ def normalize_axis_argument(
46
+ x: jnp.ndarray,
47
+ axis: int | None,
48
+ ) -> tuple[jnp.ndarray, int]:
49
+ """Flatten when ``axis`` is None, otherwise canonicalize the axis."""
50
+ if axis is None:
51
+ return jnp.ravel(x), 0
52
+ return x, canonicalize_axis(axis, x.ndim)
53
+
54
+
55
+ @overload
56
+ def standardize_and_squash(
57
+ x: jnp.ndarray,
58
+ axis: int = ...,
59
+ eps: float = ...,
60
+ temperature: float = ...,
61
+ return_mean_std: Literal[False] = ...,
62
+ ) -> jnp.ndarray: ...
63
+
64
+
65
+ @overload
66
+ def standardize_and_squash(
67
+ x: jnp.ndarray,
68
+ axis: int = ...,
69
+ eps: float = ...,
70
+ temperature: float = ...,
71
+ *,
72
+ return_mean_std: Literal[True],
73
+ ) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: ...
74
+
75
+
76
+ def standardize_and_squash(
77
+ x: jnp.ndarray,
78
+ axis: int = -1,
79
+ eps: float = 1e-6,
80
+ temperature: float = 1.0,
81
+ return_mean_std: bool = False,
82
+ ) -> jnp.ndarray | tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
83
+ """Standardize along axis then squash to (0, 1) via sigmoid.
84
+
85
+ Steps:
86
+ 1. Standardize: ``(x - mean) / std`` along ``axis``
87
+ 2. Scale by ``1 / temperature``
88
+ 3. Squash: apply sigmoid to map to (0, 1)
89
+
90
+ Args:
91
+ x: Input array.
92
+ axis: Axis along which to standardize.
93
+ eps: Epsilon for numerical stability in std computation.
94
+ temperature: Controls sharpness. Lower = sharper.
95
+ return_mean_std: If True, also return mean and std.
96
+
97
+ Returns:
98
+ Squashed array in (0, 1). If ``return_mean_std``, returns
99
+ ``(squashed, mean, std)`` tuple.
100
+ """
101
+ mean = jnp.mean(x, axis=axis, keepdims=True)
102
+ var = jnp.mean((x - mean) ** 2, axis=axis, keepdims=True)
103
+ std = jnp.sqrt(var + eps)
104
+ z = (x - mean) / std
105
+ z = z / temperature
106
+ z = jax.nn.sigmoid(z)
107
+ if return_mean_std:
108
+ return z, mean, std
109
+ return z
110
+
111
+
112
+ def unsquash_and_destandardize(
113
+ y: jnp.ndarray,
114
+ mean: jnp.ndarray,
115
+ std: jnp.ndarray,
116
+ eps: float = 1e-10,
117
+ temperature: float = 1.0,
118
+ ) -> jnp.ndarray:
119
+ """Inverse of :func:`standardize_and_squash`.
120
+
121
+ Steps:
122
+ 1. Unsquash: logit to map from (0, 1) back to R
123
+ 2. Scale by ``temperature``
124
+ 3. Destandardize: ``z * std + mean``
125
+
126
+ Args:
127
+ y: Squashed array in (0, 1).
128
+ mean: Mean from standardization.
129
+ std: Std from standardization.
130
+ eps: Clipping epsilon for logit stability.
131
+ temperature: Must match the value used in squash.
132
+
133
+ Returns:
134
+ Recovered array in original scale.
135
+ """
136
+ safe_eps = jnp.maximum(eps, 10 * jnp.finfo(y.dtype).eps)
137
+ y = jnp.clip(y, safe_eps, 1.0 - safe_eps)
138
+ z = jnp.log(y / (1.0 - y))
139
+ z = z * temperature
140
+ return z * std + mean
141
+
142
+
143
+ def quantile_interpolation_params(
144
+ q: jnp.ndarray,
145
+ n: int,
146
+ method: Literal["linear", "lower", "higher", "nearest", "midpoint"],
147
+ ) -> tuple[jnp.ndarray, jnp.ndarray, bool]:
148
+ """Compute interpolation parameters for quantile computation.
149
+
150
+ Maps a quantile ``q`` in [0, 1] to index ``k``, interpolation
151
+ weight ``a``, and whether to take the next element.
152
+
153
+ Args:
154
+ q: Quantile value(s) in [0, 1].
155
+ n: Number of elements in the sorted array.
156
+ method: Interpolation method.
157
+
158
+ Returns:
159
+ Tuple of (k, a, take_next) where k is the integer index,
160
+ a is the interpolation weight, and take_next indicates
161
+ whether to interpolate with the next element.
162
+ """
163
+ p = q * (n - 1)
164
+
165
+ if method == "linear":
166
+ k = jnp.floor(p).astype(jnp.int32)
167
+ a = p - k
168
+ take_next = True
169
+ elif method == "lower":
170
+ k = jnp.floor(p).astype(jnp.int32)
171
+ k = jnp.clip(k, 0, n - 1)
172
+ a = jnp.zeros_like(p)
173
+ take_next = False
174
+ elif method == "higher":
175
+ k = jnp.ceil(p).astype(jnp.int32)
176
+ k = jnp.clip(k, 0, n - 1)
177
+ a = jnp.zeros_like(p)
178
+ take_next = False
179
+ elif method == "nearest":
180
+ flag = jnp.less_equal(p - jnp.floor(p), 0.5)
181
+ k = jnp.where(flag, jnp.floor(p), jnp.ceil(p)).astype(jnp.int32)
182
+ a = jnp.zeros_like(p)
183
+ take_next = False
184
+ elif method == "midpoint":
185
+ k = jnp.floor(p).astype(jnp.int32)
186
+ a = jnp.full_like(p, 0.5)
187
+ is_int = jnp.isclose(p, jnp.round(p))
188
+ a = jnp.where(is_int, 0.0, a)
189
+ take_next = True
190
+ else:
191
+ msg = f"Unknown quantile method: {method!r}"
192
+ raise ValueError(msg)
193
+ return k, a, take_next
194
+
195
+
196
+ def map_in_chunks(
197
+ f,
198
+ xs: jnp.ndarray,
199
+ chunk_size: int,
200
+ ) -> jnp.ndarray:
201
+ """Map ``f`` row-wise over axis 0 using checkpointed ``lax.scan``.
202
+
203
+ ``f`` receives a chunk of shape ``(chunk_size, *rest)`` and must
204
+ return ``(chunk_size, *out_rest)``. Uses ``jax.checkpoint`` for
205
+ O(n) backward memory.
206
+
207
+ Args:
208
+ f: Function to apply to each chunk.
209
+ xs: Input array with shape ``(n, *rest)``.
210
+ chunk_size: Number of rows per chunk.
211
+
212
+ Returns:
213
+ Output array with shape ``(n, *out_rest)``.
214
+ """
215
+ n = xs.shape[0]
216
+ if chunk_size >= n:
217
+ return f(xs)
218
+ remainder = n % chunk_size
219
+ if remainder:
220
+ pad_size = chunk_size - remainder
221
+ padding = jnp.zeros((pad_size, *xs.shape[1:]), dtype=xs.dtype)
222
+ xs_padded = jnp.concatenate([xs, padding], axis=0)
223
+ else:
224
+ xs_padded = xs
225
+ n_padded = xs_padded.shape[0]
226
+ xs_chunked = xs_padded.reshape(
227
+ n_padded // chunk_size,
228
+ chunk_size,
229
+ *xs.shape[1:],
230
+ )
231
+ f_remat = jax.checkpoint(f)
232
+ _, ys = jax.lax.scan(
233
+ lambda _, chunk: (None, f_remat(chunk)),
234
+ None,
235
+ xs_chunked,
236
+ )
237
+ ys = ys.reshape(n_padded, *ys.shape[2:])
238
+ return ys[:n]
239
+
240
+
241
+ def reduce_in_chunks(
242
+ f,
243
+ xs: jnp.ndarray,
244
+ chunk_size: int,
245
+ ) -> jnp.ndarray:
246
+ """Apply ``f`` to chunks of ``xs`` along axis 0 and sum results.
247
+
248
+ ``f`` receives a chunk of shape ``(chunk_size, *rest)`` and must
249
+ return a result whose shape does **not** include the chunk
250
+ dimension. Remainder rows are zero-padded; the correction
251
+ ``f(zeros)`` is subtracted.
252
+
253
+ Uses ``jax.checkpoint`` for memory-efficient backpropagation.
254
+
255
+ Args:
256
+ f: Reduction function applied per chunk.
257
+ xs: Input array with shape ``(n, *rest)``.
258
+ chunk_size: Number of rows per chunk.
259
+
260
+ Returns:
261
+ Summed result across all chunks.
262
+ """
263
+ n = xs.shape[0]
264
+ if chunk_size >= n:
265
+ return f(xs)
266
+ remainder = n % chunk_size
267
+ if remainder:
268
+ pad_size = chunk_size - remainder
269
+ padding = jnp.zeros((pad_size, *xs.shape[1:]), dtype=xs.dtype)
270
+ xs_padded = jnp.concatenate([xs, padding], axis=0)
271
+ else:
272
+ pad_size = 0
273
+ xs_padded = xs
274
+ n_padded = xs_padded.shape[0]
275
+ xs_chunked = xs_padded.reshape(
276
+ n_padded // chunk_size,
277
+ chunk_size,
278
+ *xs.shape[1:],
279
+ )
280
+
281
+ out_struct = jax.eval_shape(f, xs_chunked[0])
282
+ init = jnp.zeros(out_struct.shape, dtype=out_struct.dtype)
283
+
284
+ f_remat = jax.checkpoint(f)
285
+
286
+ def body(acc: jnp.ndarray, chunk: jnp.ndarray):
287
+ """Accumulate reduction result for one chunk."""
288
+ return acc + f_remat(chunk), None
289
+
290
+ result, _ = jax.lax.scan(body, init, xs_chunked)
291
+
292
+ if remainder:
293
+ zero_pad = jnp.zeros((pad_size, *xs.shape[1:]), dtype=xs.dtype)
294
+ result = result - f(zero_pad)
295
+ return result
296
+
297
+
298
+ def canonicalize_axis(axis: int | None, num_dims: int) -> int:
299
+ """Normalize axis to a positive integer.
300
+
301
+ Args:
302
+ axis: Axis index (positive or negative). Must not be None.
303
+ num_dims: Number of dimensions in the array.
304
+
305
+ Returns:
306
+ Normalized positive axis index.
307
+
308
+ Raises:
309
+ ValueError: If axis is None or out of bounds.
310
+ """
311
+ if axis is None:
312
+ msg = "axis must be specified"
313
+ raise ValueError(msg)
314
+ if not -num_dims <= axis < num_dims:
315
+ msg = f"axis {axis} is out of bounds for array of dimension {num_dims}"
316
+ raise ValueError(msg)
317
+ if axis < 0:
318
+ axis += num_dims
319
+ return axis
320
+
321
+
322
+ def flatten_or_canonicalize_axis(
323
+ x: jnp.ndarray,
324
+ axis: int | None,
325
+ ) -> tuple[jnp.ndarray, int, int | None]:
326
+ """Flatten ``x`` if ``axis`` is None, otherwise canonicalize.
327
+
328
+ Returns ``(x_prepared, resolved_axis, original_ndim_if_flattened)``.
329
+ The third element is None when axis was not None.
330
+
331
+ Args:
332
+ x: Input array.
333
+ axis: Axis index or None (flatten).
334
+
335
+ Returns:
336
+ Tuple of prepared array, resolved axis, and original ndim
337
+ (only set when the array was flattened).
338
+ """
339
+ if axis is None:
340
+ num_dims = x.ndim
341
+ return jnp.ravel(x), 0, num_dims
342
+ return x, canonicalize_axis(axis, x.ndim), None
@@ -0,0 +1,120 @@
1
+ """Autograd-safe math operations.
2
+
3
+ Provides NaN-free alternatives to standard JAX math functions by using
4
+ the double-where trick: the forward pass computes the correct value
5
+ even at domain boundaries, and the backward pass produces finite (zero)
6
+ gradients instead of NaN/Inf.
7
+
8
+ The double-where trick works by:
9
+ 1. Replacing problematic inputs with safe values (e.g., 0 -> 1 for sqrt)
10
+ 2. Computing the function on the safe input
11
+ 3. Using ``jnp.where`` to select the safe output or a fallback (e.g., 0)
12
+
13
+ Because JAX traces through both branches of ``jnp.where``, step 1
14
+ ensures the "unused" branch never produces NaN in its gradient.
15
+ """
16
+
17
+ import jax.numpy as jnp
18
+ from jax import Array
19
+
20
+
21
+ def sqrt(x: Array) -> Array:
22
+ """Autograd-safe square root.
23
+
24
+ Returns ``sqrt(x)`` for ``x > 0`` and ``0`` otherwise, without
25
+ producing NaN gradients at ``x = 0``.
26
+
27
+ Args:
28
+ x: Input array.
29
+
30
+ Returns:
31
+ Elementwise square root, safe for autodiff.
32
+ """
33
+ safe_x = jnp.where(x > 0, x, 1.0)
34
+ return jnp.where(x > 0, jnp.sqrt(safe_x), 0.0)
35
+
36
+
37
+ def arcsin(x: Array) -> Array:
38
+ """Autograd-safe arcsine.
39
+
40
+ Returns ``arcsin(x)`` for ``|x| < 1`` and ``+/-pi/2`` at the
41
+ boundary, without producing NaN gradients at ``x = +/-1``.
42
+
43
+ Args:
44
+ x: Input array with values in [-1, 1].
45
+
46
+ Returns:
47
+ Elementwise arcsine, safe for autodiff.
48
+ """
49
+ interior = jnp.abs(x) < 1
50
+ safe_x = jnp.where(interior, x, 0.0)
51
+ return jnp.where(interior, jnp.arcsin(safe_x), jnp.sign(x) * (jnp.pi / 2))
52
+
53
+
54
+ def arccos(x: Array) -> Array:
55
+ """Autograd-safe arccosine.
56
+
57
+ Returns ``arccos(x)`` for ``|x| < 1``, ``0`` at ``x = 1``, and
58
+ ``pi`` at ``x = -1``, without producing NaN gradients at the
59
+ boundary.
60
+
61
+ Args:
62
+ x: Input array with values in [-1, 1].
63
+
64
+ Returns:
65
+ Elementwise arccosine, safe for autodiff.
66
+ """
67
+ interior = jnp.abs(x) < 1
68
+ safe_x = jnp.where(interior, x, 0.0)
69
+ return jnp.where(interior, jnp.arccos(safe_x), jnp.where(x >= 1, 0.0, jnp.pi))
70
+
71
+
72
+ def div(x: Array, y: Array) -> Array:
73
+ """Autograd-safe division.
74
+
75
+ Returns ``x / y`` when ``y != 0`` and ``0`` otherwise, without
76
+ producing NaN gradients at ``y = 0``.
77
+
78
+ Args:
79
+ x: Numerator array.
80
+ y: Denominator array.
81
+
82
+ Returns:
83
+ Elementwise safe division.
84
+ """
85
+ nonzero = y != 0
86
+ safe_y = jnp.where(nonzero, y, 1.0)
87
+ return jnp.where(nonzero, x / safe_y, 0.0)
88
+
89
+
90
+ def log(x: Array) -> Array:
91
+ """Autograd-safe natural logarithm.
92
+
93
+ Returns ``log(x)`` for ``x > 0`` and ``0`` otherwise, without
94
+ producing NaN gradients at ``x = 0``.
95
+
96
+ Args:
97
+ x: Input array.
98
+
99
+ Returns:
100
+ Elementwise natural logarithm, safe for autodiff.
101
+ """
102
+ safe_x = jnp.where(x > 0, x, 1.0)
103
+ return jnp.where(x > 0, jnp.log(safe_x), 0.0)
104
+
105
+
106
+ def norm(x: Array, axis: int | None = None, keepdims: bool = False) -> Array:
107
+ """Autograd-safe L2 norm.
108
+
109
+ Computes ``sqrt(sum(x**2))`` using :func:`sqrt`, avoiding NaN
110
+ gradients when the norm is zero.
111
+
112
+ Args:
113
+ x: Input array.
114
+ axis: Axis or axes along which to compute the norm.
115
+ keepdims: If True, retains reduced axes with size 1.
116
+
117
+ Returns:
118
+ L2 norm along the given axis, safe for autodiff.
119
+ """
120
+ return sqrt(jnp.sum(x * x, axis=axis, keepdims=keepdims))
@@ -0,0 +1,235 @@
1
+ """Soft comparison operators.
2
+
3
+ Provides differentiable relaxations of elementwise comparison operations
4
+ (greater, less, equal, etc.) returning SoftBool values in [0, 1].
5
+
6
+ Each function uses :func:`~diffbio.core.soft_ops.elementwise.sigmoidal`
7
+ as the underlying smooth step function, inheriting the multi-mode
8
+ smoothness options.
9
+ """
10
+
11
+ import jax.numpy as jnp
12
+ from jax import Array
13
+
14
+ from diffbio.core.soft_ops._types import SoftBool
15
+ from diffbio.core.soft_ops._utils import ensure_float
16
+ from diffbio.core.soft_ops.elementwise import Mode, abs, sigmoidal
17
+ from diffbio.core.soft_ops.logical import logical_not
18
+
19
+
20
+ def greater(
21
+ x: Array,
22
+ y: float | Array,
23
+ softness: float | Array = 0.1,
24
+ mode: Mode = "smooth",
25
+ epsilon: float = 1e-10,
26
+ ) -> SoftBool:
27
+ """Soft ``x > y``.
28
+
29
+ Uses sigmoidal on ``x - y - epsilon`` so the output approaches 0
30
+ at equality as softness -> 0.
31
+
32
+ Args:
33
+ x: First input array.
34
+ y: Second input array (broadcastable with x).
35
+ softness: Width of transition (> 0).
36
+ mode: ``"hard"`` or sigmoidal mode.
37
+ epsilon: Small offset for strict inequality at the limit.
38
+
39
+ Returns:
40
+ SoftBool in [0, 1].
41
+ """
42
+ x = ensure_float(x)
43
+ if mode == "hard":
44
+ return jnp.greater(x, y).astype(x.dtype)
45
+ return sigmoidal(x - y - epsilon, softness=softness, mode=mode)
46
+
47
+
48
+ def greater_equal(
49
+ x: Array,
50
+ y: float | Array,
51
+ softness: float | Array = 0.1,
52
+ mode: Mode = "smooth",
53
+ epsilon: float = 1e-10,
54
+ ) -> SoftBool:
55
+ """Soft ``x >= y``.
56
+
57
+ Uses sigmoidal on ``x - y + epsilon`` so the output approaches 1
58
+ at equality as softness -> 0.
59
+
60
+ Args:
61
+ x: First input array.
62
+ y: Second input array.
63
+ softness: Width of transition (> 0).
64
+ mode: ``"hard"`` or sigmoidal mode.
65
+ epsilon: Small offset for non-strict inequality at the limit.
66
+
67
+ Returns:
68
+ SoftBool in [0, 1].
69
+ """
70
+ x = ensure_float(x)
71
+ if mode == "hard":
72
+ return jnp.greater_equal(x, y).astype(x.dtype)
73
+ return sigmoidal(x - y + epsilon, softness=softness, mode=mode)
74
+
75
+
76
+ def less(
77
+ x: Array,
78
+ y: float | Array,
79
+ softness: float | Array = 0.1,
80
+ mode: Mode = "smooth",
81
+ epsilon: float = 1e-10,
82
+ ) -> SoftBool:
83
+ """Soft ``x < y``. Complement of :func:`greater_equal`.
84
+
85
+ Args:
86
+ x: First input array.
87
+ y: Second input array.
88
+ softness: Width of transition (> 0).
89
+ mode: ``"hard"`` or sigmoidal mode.
90
+ epsilon: Small offset.
91
+
92
+ Returns:
93
+ SoftBool in [0, 1].
94
+ """
95
+ x = ensure_float(x)
96
+ if mode == "hard":
97
+ return jnp.less(x, y).astype(x.dtype)
98
+ return logical_not(
99
+ greater_equal(x, y, softness=softness, mode=mode, epsilon=epsilon),
100
+ )
101
+
102
+
103
+ def less_equal(
104
+ x: Array,
105
+ y: float | Array,
106
+ softness: float | Array = 0.1,
107
+ mode: Mode = "smooth",
108
+ epsilon: float = 1e-10,
109
+ ) -> SoftBool:
110
+ """Soft ``x <= y``. Complement of :func:`greater`.
111
+
112
+ Args:
113
+ x: First input array.
114
+ y: Second input array.
115
+ softness: Width of transition (> 0).
116
+ mode: ``"hard"`` or sigmoidal mode.
117
+ epsilon: Small offset.
118
+
119
+ Returns:
120
+ SoftBool in [0, 1].
121
+ """
122
+ x = ensure_float(x)
123
+ if mode == "hard":
124
+ return jnp.less_equal(x, y).astype(x.dtype)
125
+ return logical_not(
126
+ greater(x, y, softness=softness, mode=mode, epsilon=epsilon),
127
+ )
128
+
129
+
130
+ def equal(
131
+ x: Array,
132
+ y: Array,
133
+ softness: float | Array = 0.1,
134
+ mode: Mode = "smooth",
135
+ epsilon: float = 1e-10,
136
+ ) -> SoftBool:
137
+ """Soft ``x == y``.
138
+
139
+ Implemented as soft ``abs(x - y) <= 0``, scaled to [0, 1].
140
+
141
+ Args:
142
+ x: First input array.
143
+ y: Second input array.
144
+ softness: Width of transition (> 0).
145
+ mode: ``"hard"`` or sigmoidal mode.
146
+ epsilon: Small offset.
147
+
148
+ Returns:
149
+ SoftBool in [0, 1].
150
+ """
151
+ x = ensure_float(x)
152
+ if mode == "hard":
153
+ return jnp.equal(x, y).astype(x.dtype)
154
+ diff = abs(x - y, softness=softness, mode=mode)
155
+ return 2.0 * less_equal(
156
+ diff,
157
+ jnp.zeros_like(diff),
158
+ mode=mode,
159
+ softness=softness,
160
+ epsilon=epsilon,
161
+ )
162
+
163
+
164
+ def not_equal(
165
+ x: Array,
166
+ y: Array,
167
+ softness: float | Array = 0.1,
168
+ mode: Mode = "smooth",
169
+ epsilon: float = 1e-10,
170
+ ) -> SoftBool:
171
+ """Soft ``x != y``.
172
+
173
+ Implemented as soft ``abs(x - y) > 0``, scaled to [0, 1].
174
+
175
+ Args:
176
+ x: First input array.
177
+ y: Second input array.
178
+ softness: Width of transition (> 0).
179
+ mode: ``"hard"`` or sigmoidal mode.
180
+ epsilon: Small offset.
181
+
182
+ Returns:
183
+ SoftBool in [0, 1].
184
+ """
185
+ x = ensure_float(x)
186
+ if mode == "hard":
187
+ return jnp.not_equal(x, y).astype(x.dtype)
188
+ diff = abs(x - y, softness=softness, mode=mode)
189
+ tmp = greater(
190
+ diff,
191
+ jnp.zeros_like(diff),
192
+ mode=mode,
193
+ softness=softness,
194
+ epsilon=epsilon,
195
+ )
196
+ return 2.0 * tmp - 1.0
197
+
198
+
199
+ def isclose(
200
+ x: Array,
201
+ y: Array,
202
+ softness: float | Array = 0.1,
203
+ rtol: float = 1e-05,
204
+ atol: float = 1e-08,
205
+ mode: Mode = "smooth",
206
+ epsilon: float = 1e-10,
207
+ ) -> SoftBool:
208
+ """Soft approximate equality.
209
+
210
+ Implements soft ``abs(x - y) <= atol + rtol * abs(y)``.
211
+
212
+ Args:
213
+ x: First input array.
214
+ y: Second input array.
215
+ softness: Width of transition (> 0).
216
+ rtol: Relative tolerance.
217
+ atol: Absolute tolerance.
218
+ mode: ``"hard"`` or sigmoidal mode.
219
+ epsilon: Small offset.
220
+
221
+ Returns:
222
+ SoftBool in [0, 1].
223
+ """
224
+ x = ensure_float(x)
225
+ if mode == "hard":
226
+ return jnp.isclose(x, y, atol=atol, rtol=rtol).astype(x.dtype)
227
+ diff = abs(x - y, softness=softness, mode=mode)
228
+ y_abs = abs(y, softness=softness, mode=mode)
229
+ return 2.0 * less_equal(
230
+ diff,
231
+ atol + rtol * y_abs,
232
+ mode=mode,
233
+ softness=softness,
234
+ epsilon=epsilon,
235
+ )