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,376 @@
1
+ """Soft quantile, median, and percentile operators.
2
+
3
+ Provides differentiable relaxations of quantile-based statistics.
4
+ Quantiles are computed via :func:`~diffbio.core.soft_ops.sorting.argsort`
5
+ or :func:`~diffbio.core.soft_ops.sorting.sort`, with interpolation
6
+ following the same methods as ``jax.numpy.quantile``.
7
+ """
8
+
9
+ from typing import Literal, cast
10
+
11
+ import jax
12
+ import jax.numpy as jnp
13
+ from jax import Array
14
+
15
+ from diffbio.core.soft_ops._projections_simplex import SimplexMode, proj_simplex
16
+ from diffbio.core.soft_ops._types import SoftIndex
17
+ from diffbio.core.soft_ops._utils import (
18
+ canonicalize_axis,
19
+ ensure_float,
20
+ quantile_interpolation_params,
21
+ standardize_and_squash,
22
+ )
23
+ from diffbio.core.soft_ops.selection import take_along_axis
24
+ from diffbio.core.soft_ops.sorting import (
25
+ _neuralsort_a_sum,
26
+ _sorting_network_permutation,
27
+ )
28
+
29
+ Mode = Literal["hard", "smooth", "c0", "c1", "c2"]
30
+ ArgMethod = Literal["softsort", "neuralsort", "sorting_network"]
31
+
32
+
33
+ def _sorting_network_quantile_index(
34
+ x_last: Array,
35
+ softness: float | Array,
36
+ mode: Mode,
37
+ *,
38
+ standardize: bool,
39
+ take_next: bool,
40
+ a_b: Array,
41
+ k: Array,
42
+ kp1: Array,
43
+ ) -> Array:
44
+ """Return the soft quantile index from the sorting-network backend."""
45
+ perm = _sorting_network_permutation(
46
+ x_last,
47
+ softness,
48
+ cast(SimplexMode, mode),
49
+ descending=False,
50
+ standardized=standardize,
51
+ )
52
+ if take_next:
53
+ return (1.0 - a_b) * perm[..., k, :] + a_b * perm[..., kp1, :]
54
+ return perm[..., k, :]
55
+
56
+
57
+ def argquantile(
58
+ x: Array,
59
+ q: Array,
60
+ axis: int | None = None,
61
+ keepdims: bool = False,
62
+ softness: float | Array = 0.1,
63
+ mode: Mode = "smooth",
64
+ method: ArgMethod = "neuralsort",
65
+ quantile_method: Literal[
66
+ "linear",
67
+ "lower",
68
+ "higher",
69
+ "nearest",
70
+ "midpoint",
71
+ ] = "linear",
72
+ standardize: bool = True,
73
+ ) -> SoftIndex:
74
+ """Soft argquantile returning SoftIndex.
75
+
76
+ Args:
77
+ x: Input array.
78
+ q: Quantile(s) in [0, 1]. Scalar or 1-D array.
79
+ axis: Axis along which to compute. None flattens.
80
+ keepdims: If True, keep reduced dimension.
81
+ softness: Controls sharpness (> 0).
82
+ mode: Smoothness mode.
83
+ method: Algorithm.
84
+ quantile_method: Interpolation method.
85
+ standardize: If True, standardize input.
86
+
87
+ Returns:
88
+ SoftIndex probability distribution over quantile position(s).
89
+ """
90
+ q_arr = jnp.asarray(q)
91
+ if q_arr.ndim > 1:
92
+ msg = f"q must be scalar or 1-D, got shape {q_arr.shape}"
93
+ raise ValueError(msg)
94
+ if q_arr.ndim == 1:
95
+
96
+ def _single(qi: Array) -> SoftIndex:
97
+ return argquantile(
98
+ x,
99
+ q=qi,
100
+ axis=axis,
101
+ keepdims=keepdims,
102
+ softness=softness,
103
+ mode=mode,
104
+ method=method,
105
+ quantile_method=quantile_method,
106
+ standardize=standardize,
107
+ )
108
+
109
+ return jax.vmap(_single)(q_arr)
110
+
111
+ orig_axis_is_none = axis is None
112
+ if axis is None:
113
+ num_dims = x.ndim
114
+ x = jnp.ravel(x)
115
+ axis = 0
116
+ else:
117
+ axis = canonicalize_axis(axis, x.ndim)
118
+ num_dims = None
119
+
120
+ if mode != "hard":
121
+ x = ensure_float(x)
122
+ if standardize and mode != "hard":
123
+ x = standardize_and_squash(x, axis=axis)
124
+
125
+ x_last = jnp.moveaxis(x, axis, -1)
126
+ *batch_dims, n = x_last.shape
127
+
128
+ q_val = jnp.clip(q, 0.0, 1.0)
129
+ k, a, take_next = quantile_interpolation_params(q_val, n, quantile_method)
130
+ a_b = jnp.expand_dims(a, axis=-1)
131
+ kp1 = jnp.minimum(k + 1, n - 1)
132
+
133
+ if mode == "hard":
134
+ indices = jnp.argsort(x_last, axis=-1, descending=False)
135
+ if take_next:
136
+ idx_pair = jnp.stack(
137
+ [indices[..., k], indices[..., kp1]],
138
+ axis=-1,
139
+ )
140
+ oh = jax.nn.one_hot(idx_pair, num_classes=n, axis=-1)
141
+ soft_index = (1.0 - a_b) * oh[..., 0, :] + a_b * oh[..., 1, :]
142
+ else:
143
+ soft_index = jax.nn.one_hot(
144
+ indices[..., k],
145
+ num_classes=n,
146
+ axis=-1,
147
+ )
148
+ elif method == "softsort":
149
+ x_sorted = jnp.sort(x_last, axis=-1, descending=False)
150
+ if take_next:
151
+ anchors = jnp.stack(
152
+ [x_sorted[..., k], x_sorted[..., kp1]],
153
+ axis=-1,
154
+ )
155
+ abs_diff = jnp.abs(
156
+ anchors[..., :, None] - x_last[..., None, :],
157
+ )
158
+ proj = proj_simplex(-abs_diff, axis=-1, softness=softness, mode=mode)
159
+ soft_index = (1.0 - a_b) * proj[..., 0, :] + a_b * proj[..., 1, :]
160
+ else:
161
+ anchors = x_sorted[..., k, None]
162
+ abs_diff = jnp.abs(
163
+ anchors[..., :, None] - x_last[..., None, :],
164
+ )
165
+ soft_index = proj_simplex(
166
+ -abs_diff,
167
+ axis=-1,
168
+ softness=softness,
169
+ mode=mode,
170
+ )[..., 0, :]
171
+ elif method == "neuralsort":
172
+ a_sum = _neuralsort_a_sum(x_last, mode=mode, softness=softness)
173
+ if take_next:
174
+ i = jnp.array([k + 1, k + 2])
175
+ coef = n + 1 - 2 * i
176
+ coef = jnp.broadcast_to(coef, (*batch_dims, 2))
177
+ z = -(coef[..., :, None] * x_last[..., None, :] + a_sum[..., None, :])
178
+ proj = proj_simplex(z, axis=-1, softness=softness, mode=mode)
179
+ soft_index = (1.0 - a_b) * proj[..., 0, :] + a_b * proj[..., 1, :]
180
+ else:
181
+ coef = jnp.array([n + 1 - 2 * (k + 1)])
182
+ coef = jnp.broadcast_to(coef, (*batch_dims, 1))
183
+ z = -(coef[..., :, None] * x_last[..., None, :] + a_sum[..., None, :])
184
+ soft_index = proj_simplex(
185
+ z,
186
+ axis=-1,
187
+ softness=softness,
188
+ mode=mode,
189
+ )[..., 0, :]
190
+ elif method == "sorting_network":
191
+ soft_index = _sorting_network_quantile_index(
192
+ x_last,
193
+ softness,
194
+ mode,
195
+ standardize=standardize,
196
+ take_next=take_next,
197
+ a_b=a_b,
198
+ k=k,
199
+ kp1=kp1,
200
+ )
201
+ else:
202
+ msg = f"Invalid method: {method!r}"
203
+ raise ValueError(msg)
204
+
205
+ if keepdims:
206
+ if orig_axis_is_none:
207
+ soft_index = soft_index.reshape(*(1,) * num_dims, n)
208
+ else:
209
+ soft_index = jnp.expand_dims(soft_index, axis=axis)
210
+
211
+ return soft_index
212
+
213
+
214
+ def quantile(
215
+ x: Array,
216
+ q: Array,
217
+ axis: int | None = None,
218
+ keepdims: bool = False,
219
+ softness: float | Array = 0.1,
220
+ mode: Mode = "smooth",
221
+ method: ArgMethod = "neuralsort",
222
+ quantile_method: Literal[
223
+ "linear",
224
+ "lower",
225
+ "higher",
226
+ "nearest",
227
+ "midpoint",
228
+ ] = "linear",
229
+ standardize: bool = True,
230
+ gated_grad: bool = True,
231
+ ) -> Array:
232
+ """Soft quantile returning value.
233
+
234
+ Implemented as :func:`argquantile` + :func:`take_along_axis`
235
+ for most methods.
236
+
237
+ Args:
238
+ x: Input array.
239
+ q: Quantile(s) in [0, 1].
240
+ axis: Axis along which to compute.
241
+ keepdims: If True, keep reduced dimension.
242
+ softness: Controls sharpness (> 0).
243
+ mode: Smoothness mode.
244
+ method: Algorithm.
245
+ quantile_method: Interpolation method.
246
+ standardize: If True, standardize input.
247
+ gated_grad: If False, stop gradient through soft index.
248
+
249
+ Returns:
250
+ Quantile value(s).
251
+ """
252
+ if mode == "hard":
253
+ return jnp.quantile(x, q, axis=axis, keepdims=keepdims, method=quantile_method)
254
+
255
+ soft_idx = argquantile(
256
+ x,
257
+ q,
258
+ axis=axis,
259
+ keepdims=True,
260
+ softness=softness,
261
+ mode=mode,
262
+ method=method,
263
+ quantile_method=quantile_method,
264
+ standardize=standardize,
265
+ )
266
+ if not gated_grad:
267
+ soft_idx = jax.lax.stop_gradient(soft_idx)
268
+
269
+ _axis = 0 if axis is None else canonicalize_axis(axis, x.ndim)
270
+ if axis is None:
271
+ x = jnp.ravel(x)
272
+
273
+ q_arr = jnp.asarray(q)
274
+ if q_arr.ndim == 1:
275
+ result = jax.vmap(lambda si: take_along_axis(x, si, axis=_axis))(soft_idx)
276
+ else:
277
+ result = take_along_axis(x, soft_idx, axis=_axis)
278
+
279
+ if not keepdims:
280
+ if q_arr.ndim == 0:
281
+ result = jnp.squeeze(result, axis=_axis if axis is not None else 0)
282
+ return result
283
+
284
+
285
+ def argmedian(
286
+ x: Array,
287
+ axis: int | None = None,
288
+ keepdims: bool = False,
289
+ softness: float | Array = 0.1,
290
+ mode: Mode = "smooth",
291
+ method: ArgMethod = "neuralsort",
292
+ standardize: bool = True,
293
+ ) -> SoftIndex:
294
+ """Soft argmedian: :func:`argquantile` with ``q=0.5``."""
295
+ return argquantile(
296
+ x,
297
+ q=jnp.array(0.5),
298
+ axis=axis,
299
+ keepdims=keepdims,
300
+ softness=softness,
301
+ mode=mode,
302
+ method=method,
303
+ standardize=standardize,
304
+ )
305
+
306
+
307
+ def median(
308
+ x: Array,
309
+ axis: int | None = None,
310
+ keepdims: bool = False,
311
+ softness: float | Array = 0.1,
312
+ mode: Mode = "smooth",
313
+ method: ArgMethod = "neuralsort",
314
+ standardize: bool = True,
315
+ gated_grad: bool = True,
316
+ ) -> Array:
317
+ """Soft median: :func:`quantile` with ``q=0.5``."""
318
+ return quantile(
319
+ x,
320
+ q=jnp.array(0.5),
321
+ axis=axis,
322
+ keepdims=keepdims,
323
+ softness=softness,
324
+ mode=mode,
325
+ method=method,
326
+ standardize=standardize,
327
+ gated_grad=gated_grad,
328
+ )
329
+
330
+
331
+ def argpercentile(
332
+ x: Array,
333
+ p: Array,
334
+ axis: int | None = None,
335
+ keepdims: bool = False,
336
+ softness: float | Array = 0.1,
337
+ mode: Mode = "smooth",
338
+ method: ArgMethod = "neuralsort",
339
+ standardize: bool = True,
340
+ ) -> SoftIndex:
341
+ """Soft argpercentile: :func:`argquantile` with ``q = p / 100``."""
342
+ return argquantile(
343
+ x,
344
+ q=jnp.asarray(p) / 100.0,
345
+ axis=axis,
346
+ keepdims=keepdims,
347
+ softness=softness,
348
+ mode=mode,
349
+ method=method,
350
+ standardize=standardize,
351
+ )
352
+
353
+
354
+ def percentile(
355
+ x: Array,
356
+ p: Array,
357
+ axis: int | None = None,
358
+ keepdims: bool = False,
359
+ softness: float | Array = 0.1,
360
+ mode: Mode = "smooth",
361
+ method: ArgMethod = "neuralsort",
362
+ standardize: bool = True,
363
+ gated_grad: bool = True,
364
+ ) -> Array:
365
+ """Soft percentile: :func:`quantile` with ``q = p / 100``."""
366
+ return quantile(
367
+ x,
368
+ q=jnp.asarray(p) / 100.0,
369
+ axis=axis,
370
+ keepdims=keepdims,
371
+ softness=softness,
372
+ mode=mode,
373
+ method=method,
374
+ standardize=standardize,
375
+ gated_grad=gated_grad,
376
+ )
@@ -0,0 +1,236 @@
1
+ """Soft selection and indexing operators.
2
+
3
+ Provides differentiable relaxations of array selection operations using
4
+ SoftBool conditions and SoftIndex probability distributions instead of
5
+ discrete boolean masks and integer indices.
6
+
7
+ Key types:
8
+ - **SoftBool**: Probability in [0, 1], used in :func:`where`.
9
+ - **SoftIndex**: Probability distribution over indices (sums to 1),
10
+ used in :func:`take_along_axis`, :func:`take`, :func:`choose`, etc.
11
+ """
12
+
13
+ from collections.abc import Sequence
14
+
15
+ import jax
16
+ import jax.numpy as jnp
17
+ from jax import Array
18
+
19
+ from diffbio.core.soft_ops._types import SoftBool, SoftIndex
20
+ from diffbio.core.soft_ops._utils import canonicalize_axis, normalize_axis_argument
21
+
22
+
23
+ def where(condition: SoftBool, x: Array, y: Array) -> Array:
24
+ """Soft where: ``x * condition + y * (1 - condition)``.
25
+
26
+ Unlike ``jnp.where``, this smoothly interpolates between ``x`` and
27
+ ``y`` based on the continuous condition value.
28
+
29
+ Args:
30
+ condition: SoftBool in [0, 1], same shape as x and y.
31
+ x: Values selected when condition is 1.
32
+ y: Values selected when condition is 0.
33
+
34
+ Returns:
35
+ Interpolated array.
36
+ """
37
+ return x * condition + y * (1.0 - condition)
38
+
39
+
40
+ def take_along_axis(
41
+ x: Array,
42
+ soft_index: SoftIndex,
43
+ axis: int | None = -1,
44
+ ) -> Array:
45
+ """Soft take_along_axis via weighted dot product.
46
+
47
+ ``soft_index`` must have one more dimension than ``x``: the extra
48
+ (last) dimension contains the probability distribution over the
49
+ elements along ``axis``.
50
+
51
+ Args:
52
+ x: Input array of shape ``(..., n, ...)``.
53
+ soft_index: SoftIndex of shape ``(..., k, ..., [n])`` where
54
+ ``[n]`` is the probability distribution dimension.
55
+ axis: Axis in ``x`` to select from. If None, ``x`` is flattened.
56
+
57
+ Returns:
58
+ Array of shape ``(..., k, ...)``.
59
+ """
60
+ x, axis = normalize_axis_argument(x, axis)
61
+ if x.ndim + 1 != soft_index.ndim:
62
+ msg = (
63
+ f"x.ndim + 1 == soft_index.ndim required, "
64
+ f"got x.ndim={x.ndim}, soft_index.ndim={soft_index.ndim}"
65
+ )
66
+ raise ValueError(msg)
67
+ x = jnp.moveaxis(x, axis, -1)
68
+ soft_index = jnp.moveaxis(soft_index, axis, -2)
69
+ dotprod = jnp.einsum("...n,...kn->...k", x, soft_index)
70
+ return jnp.moveaxis(dotprod, -1, axis)
71
+
72
+
73
+ def take(
74
+ x: Array,
75
+ soft_index: SoftIndex,
76
+ axis: int | None = None,
77
+ ) -> Array:
78
+ """Soft take via weighted dot product.
79
+
80
+ Unlike :func:`take_along_axis`, ``soft_index`` is a 2-D matrix
81
+ of shape ``(k, [n])`` applied uniformly across batch dimensions.
82
+
83
+ Args:
84
+ x: Input array of shape ``(..., n, ...)``.
85
+ soft_index: SoftIndex of shape ``(k, [n])``.
86
+ axis: Axis to select from. If None, ``x`` is flattened.
87
+
88
+ Returns:
89
+ Array of shape ``(..., k, ...)``.
90
+ """
91
+ if soft_index.ndim != 2:
92
+ msg = f"soft_index must be (k, [n]), got shape {soft_index.shape}"
93
+ raise ValueError(msg)
94
+ x, axis = normalize_axis_argument(x, axis)
95
+ if axis != x.ndim - 1:
96
+ x = jnp.moveaxis(x, axis, -1)
97
+ soft_index = jnp.reshape(
98
+ soft_index,
99
+ (1,) * (x.ndim - 1) + soft_index.shape,
100
+ )
101
+ x = jnp.expand_dims(x, axis)
102
+ soft_index = jnp.moveaxis(soft_index, -2, axis)
103
+ return jnp.sum(x * soft_index, axis=-1)
104
+
105
+
106
+ def choose(
107
+ soft_index: SoftIndex,
108
+ choices: Array,
109
+ ) -> Array:
110
+ """Soft choose among multiple arrays.
111
+
112
+ Softly selects among ``choices`` using ``soft_index`` weights.
113
+
114
+ Args:
115
+ soft_index: SoftIndex of shape ``(..., [n])``.
116
+ choices: Array of shape ``(n, ...)``.
117
+
118
+ Returns:
119
+ Weighted combination of choices.
120
+ """
121
+ if soft_index.ndim != choices.ndim or soft_index.shape[-1] != choices.shape[0]:
122
+ msg = (
123
+ f"Incompatible shapes: soft_index={soft_index.shape}, "
124
+ f"choices={choices.shape}. Need soft_index.shape=(..., [n]) "
125
+ f"and choices.shape=(n, ...)"
126
+ )
127
+ raise ValueError(msg)
128
+ tgt_shape = jnp.broadcast_shapes(choices.shape[1:], soft_index.shape[:-1])
129
+ choices_bcast = jnp.broadcast_to(choices, (choices.shape[0], *tgt_shape))
130
+ choices_bcast = jnp.moveaxis(choices_bcast, 0, -1)
131
+ return jnp.sum(choices_bcast * soft_index, axis=-1)
132
+
133
+
134
+ def dynamic_index_in_dim(
135
+ x: Array,
136
+ soft_index: SoftIndex,
137
+ axis: int = 0,
138
+ keepdims: bool = True,
139
+ ) -> Array:
140
+ """Soft dynamic indexing along a dimension.
141
+
142
+ Selects a single element (weighted combination) along ``axis``
143
+ using the probability distribution ``soft_index``.
144
+
145
+ Args:
146
+ x: Input array of shape ``(..., n, ...)``.
147
+ soft_index: SoftIndex of shape ``([n],)``.
148
+ axis: Axis to index.
149
+ keepdims: If True, retains the indexed dimension as size 1.
150
+
151
+ Returns:
152
+ Indexed array.
153
+ """
154
+ axis = canonicalize_axis(axis, x.ndim)
155
+ if x.shape[axis] != soft_index.shape[0]:
156
+ msg = (
157
+ f"Dimension mismatch: x.shape[{axis}]={x.shape[axis]} "
158
+ f"vs soft_index.shape[0]={soft_index.shape[0]}"
159
+ )
160
+ raise ValueError(msg)
161
+ x = jnp.moveaxis(x, axis, -1)
162
+ x_reshaped = jnp.reshape(x, (-1, x.shape[-1]))
163
+ dotprod = jnp.sum(x_reshaped * soft_index[None, :], axis=-1)
164
+ y = jnp.reshape(dotprod, x.shape[:-1])
165
+ if keepdims:
166
+ y = jnp.expand_dims(y, axis=axis)
167
+ return y
168
+
169
+
170
+ def dynamic_slice_in_dim(
171
+ x: Array,
172
+ soft_start_index: SoftIndex,
173
+ slice_size: int,
174
+ axis: int = 0,
175
+ ) -> Array:
176
+ """Soft dynamic slicing along a dimension.
177
+
178
+ Extracts a soft slice of ``slice_size`` elements starting at the
179
+ position defined by ``soft_start_index``.
180
+
181
+ Args:
182
+ x: Input array of shape ``(..., n, ...)``.
183
+ soft_start_index: SoftIndex of shape ``([n],)``.
184
+ slice_size: Number of elements to extract.
185
+ axis: Axis to slice.
186
+
187
+ Returns:
188
+ Array of shape ``(..., slice_size, ...)``.
189
+ """
190
+ axis = canonicalize_axis(axis, x.ndim)
191
+ if not (0 < slice_size <= x.shape[axis]):
192
+ msg = (
193
+ f"slice_size must satisfy 0 < slice_size <= x.shape[axis], "
194
+ f"got slice_size={slice_size}, x.shape[axis]={x.shape[axis]}"
195
+ )
196
+ raise ValueError(msg)
197
+
198
+ x_last = jnp.moveaxis(x, axis, -1)
199
+ t_idx = jnp.arange(slice_size)
200
+
201
+ def one_step(t: Array) -> Array:
202
+ rolled = jnp.roll(x_last, shift=-t, axis=-1)
203
+ return jnp.einsum("...n,n->...", rolled, soft_start_index)
204
+
205
+ y_stack = jax.vmap(one_step)(t_idx)
206
+ y_last = jnp.moveaxis(y_stack, 0, -1)
207
+ return jnp.moveaxis(y_last, -1, axis)
208
+
209
+
210
+ def dynamic_slice(
211
+ x: Array,
212
+ soft_start_indices: Sequence[SoftIndex],
213
+ slice_sizes: Sequence[int],
214
+ ) -> Array:
215
+ """Soft dynamic slicing across multiple dimensions.
216
+
217
+ Applies :func:`dynamic_slice_in_dim` sequentially along each axis.
218
+
219
+ Args:
220
+ x: Input array of shape ``(n_1, n_2, ..., n_k)``.
221
+ soft_start_indices: One SoftIndex per dimension.
222
+ slice_sizes: One slice length per dimension.
223
+
224
+ Returns:
225
+ Array of shape ``(l_1, l_2, ..., l_k)``.
226
+ """
227
+ if not (len(soft_start_indices) == len(slice_sizes) == x.ndim):
228
+ msg = (
229
+ f"len(soft_start_indices) == len(slice_sizes) == x.ndim required, "
230
+ f"got {len(soft_start_indices)}, {len(slice_sizes)}, {x.ndim}"
231
+ )
232
+ raise ValueError(msg)
233
+ y = x
234
+ for axis, (start, size) in enumerate(zip(soft_start_indices, slice_sizes)):
235
+ y = dynamic_slice_in_dim(y, start, size, axis=axis)
236
+ return y