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,926 @@
1
+ """Soft sorting, argmax/argmin, argsort, rank, and top-k operators.
2
+
3
+ Provides differentiable relaxations of discrete ordering operations
4
+ using multiple algorithmic approaches:
5
+
6
+ - **softsort**: Simplex projection (O(n log n)). Default for argmax/argmin.
7
+ - **neuralsort**: Pairwise comparison + simplex projection (O(n^2)).
8
+ Default for argsort/sort.
9
+ - **sorting_network**: Bitonic sorting network (O(n log^2 n)).
10
+ - **ot**: Optimal transport projection (requires optional deps).
11
+ - **fast_soft_sort**: Permutahedron projection via PAV (requires optional deps).
12
+ - **smooth_sort**: Smooth permutahedron via ESP bounds (requires optional deps).
13
+
14
+ The ``ot``, ``fast_soft_sort``, and ``smooth_sort`` methods require
15
+ the ``soft-ops-advanced`` optional dependency group. They raise
16
+ ``ImportError`` with a helpful message if called without installation.
17
+ """
18
+
19
+ from typing import Literal
20
+
21
+ import jax
22
+ import jax.numpy as jnp
23
+ from jax import Array
24
+
25
+ from diffbio.core.soft_ops._projections_simplex import SimplexMode, proj_simplex
26
+ from diffbio.core.soft_ops._sorting_network import (
27
+ argsort_via_sorting_network,
28
+ sort_via_sorting_network,
29
+ )
30
+ from diffbio.core.soft_ops._types import SoftIndex
31
+ from diffbio.core.soft_ops._utils import (
32
+ canonicalize_axis,
33
+ ensure_float,
34
+ map_in_chunks,
35
+ normalize_axis_argument,
36
+ reduce_in_chunks,
37
+ standardize_and_squash,
38
+ unsquash_and_destandardize,
39
+ )
40
+ from diffbio.core.soft_ops.elementwise import abs as soft_abs
41
+ from diffbio.core.soft_ops.selection import take_along_axis
42
+
43
+ # Optional-dependency imports (permutahedron + transport polytope).
44
+ # These are lazy-loaded at call time to avoid ImportError at import.
45
+ _ADVANCED_INSTALL_MSG = "Install with: uv pip install -e '.[soft-ops-advanced]'"
46
+
47
+
48
+ def _get_proj_permutahedron():
49
+ """Lazy import of permutahedron projection."""
50
+ from diffbio.core.soft_ops._projections_permutahedron import (
51
+ proj_permutahedron,
52
+ )
53
+
54
+ return proj_permutahedron
55
+
56
+
57
+ def _get_proj_permutahedron_smooth_sort():
58
+ """Lazy import of smooth sort permutahedron projection."""
59
+ from diffbio.core.soft_ops._projections_permutahedron import (
60
+ proj_permutahedron_smooth_sort,
61
+ )
62
+
63
+ return proj_permutahedron_smooth_sort
64
+
65
+
66
+ def _get_proj_transport_polytope():
67
+ """Lazy import of transport polytope projection."""
68
+ from diffbio.core.soft_ops._projections_transport import (
69
+ proj_transport_polytope,
70
+ )
71
+
72
+ return proj_transport_polytope
73
+
74
+
75
+ Mode = Literal["hard", "smooth", "c0", "c1", "c2"]
76
+ ArgMethod = Literal["softsort", "neuralsort", "sorting_network", "ot"]
77
+ RankMethod = Literal["softsort", "neuralsort"]
78
+ SortMethod = Literal[
79
+ "softsort",
80
+ "neuralsort",
81
+ "sorting_network",
82
+ "ot",
83
+ "fast_soft_sort",
84
+ "smooth_sort",
85
+ ]
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Internal helpers
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ def _neuralsort_a_sum(
94
+ x_last: Array,
95
+ mode: SimplexMode,
96
+ softness: float | Array,
97
+ ) -> Array:
98
+ """``a_sum[..., j] = sum_i soft_abs(x[..., i] - x[..., j])``."""
99
+ n = x_last.shape[-1]
100
+ x_flat = x_last.reshape(-1, n)
101
+
102
+ def _single(x_row: Array) -> Array:
103
+ """Compute pairwise absolute difference sums for one row."""
104
+
105
+ def _chunk_fn(x_chunk_j: Array) -> Array:
106
+ """Sum absolute differences against a chunk of columns."""
107
+ return soft_abs(
108
+ x_row[:, None] - x_chunk_j[None, :],
109
+ mode=mode,
110
+ softness=softness,
111
+ ).sum(axis=0)
112
+
113
+ return map_in_chunks(f=_chunk_fn, xs=x_row, chunk_size=128)
114
+
115
+ return jax.vmap(_single)(x_flat).reshape(x_last.shape)
116
+
117
+
118
+ def _sorting_network_permutation(
119
+ x_last: Array,
120
+ softness: float | Array,
121
+ mode: SimplexMode,
122
+ *,
123
+ descending: bool,
124
+ standardized: bool,
125
+ ) -> Array:
126
+ """Return the differentiable permutation from the sorting-network backend."""
127
+ return argsort_via_sorting_network(
128
+ x_last,
129
+ softness,
130
+ mode,
131
+ descending=descending,
132
+ standardized=standardized,
133
+ )
134
+
135
+
136
+ def _sorting_network_argmax_index(
137
+ x_last: Array,
138
+ softness: float | Array,
139
+ mode: SimplexMode,
140
+ *,
141
+ standardize: bool,
142
+ ) -> Array:
143
+ """Return the soft argmax index from the sorting-network backend."""
144
+ perm = _sorting_network_permutation(
145
+ x_last,
146
+ softness,
147
+ mode,
148
+ descending=True,
149
+ standardized=standardize,
150
+ )
151
+ return perm[..., 0, :]
152
+
153
+
154
+ def _softsort_fused_sort(
155
+ x_last: Array,
156
+ batch_dims: list[int],
157
+ softness: float | Array,
158
+ mode: SimplexMode,
159
+ descending: bool,
160
+ standardize: bool,
161
+ gated_grad: bool,
162
+ ) -> Array:
163
+ """Sorted values via SoftSort, O(n) memory."""
164
+ n = x_last.shape[-1]
165
+ x_std = standardize_and_squash(x_last, axis=-1) if standardize else x_last
166
+ x_orig_flat = x_last.reshape(-1, n)
167
+ x_std_flat = x_std.reshape(-1, n)
168
+
169
+ def _single(x_orig_row: Array, x_std_row: Array) -> Array:
170
+ """Compute soft-sorted values for a single row via SoftSort."""
171
+
172
+ def _chunk_fn(anchors_chunk: Array) -> Array:
173
+ """Project an anchor chunk onto the simplex and gather values."""
174
+ diff = jnp.abs(anchors_chunk[:, None] - x_std_row[None, :])
175
+ p_chunk = proj_simplex(-diff, axis=-1, softness=softness, mode=mode)
176
+ if not gated_grad:
177
+ p_chunk = jax.lax.stop_gradient(p_chunk)
178
+ return jnp.einsum("cn,n->c", p_chunk, x_orig_row)
179
+
180
+ anchors_row = jnp.sort(x_std_row, descending=descending)
181
+ return map_in_chunks(f=_chunk_fn, xs=anchors_row, chunk_size=128)
182
+
183
+ result = jax.vmap(_single)(x_orig_flat, x_std_flat)
184
+ return result.reshape(*batch_dims, n)
185
+
186
+
187
+ def _neuralsort_fused_sort(
188
+ x_last: Array,
189
+ batch_dims: list[int],
190
+ softness: float | Array,
191
+ mode: SimplexMode,
192
+ descending: bool,
193
+ standardize: bool,
194
+ gated_grad: bool,
195
+ ) -> Array:
196
+ """Sorted values via NeuralSort, O(n) memory."""
197
+ n = x_last.shape[-1]
198
+ x_std = standardize_and_squash(x_last, axis=-1) if standardize else x_last
199
+ a_sum = _neuralsort_a_sum(x_last=x_std, mode=mode, softness=softness)
200
+
201
+ i = jnp.arange(1, n + 1)
202
+ if descending:
203
+ i = i[::-1]
204
+ coef = n + 1 - 2 * i
205
+ coef = jnp.broadcast_to(coef, (*batch_dims, n))
206
+
207
+ x_orig_flat = x_last.reshape(-1, n)
208
+ x_std_flat = x_std.reshape(-1, n)
209
+ a_sum_flat = a_sum.reshape(-1, n)
210
+ coef_flat = coef.reshape(-1, n)
211
+
212
+ def _single(
213
+ x_orig_row: Array,
214
+ x_std_row: Array,
215
+ a_sum_row: Array,
216
+ coef_row: Array,
217
+ ) -> Array:
218
+ """Compute soft-sorted values for a single row via NeuralSort."""
219
+
220
+ def _chunk_fn(coef_chunk: Array) -> Array:
221
+ """Project a coefficient chunk onto the simplex and gather values."""
222
+ z_chunk = -(coef_chunk[:, None] * x_std_row[None, :] + a_sum_row[None, :])
223
+ p_chunk = proj_simplex(z_chunk, axis=-1, softness=softness, mode=mode)
224
+ if not gated_grad:
225
+ p_chunk = jax.lax.stop_gradient(p_chunk)
226
+ return jnp.einsum("cn,n->c", p_chunk, x_orig_row)
227
+
228
+ return map_in_chunks(f=_chunk_fn, xs=coef_row, chunk_size=128)
229
+
230
+ result = jax.vmap(_single)(
231
+ x_orig_flat,
232
+ x_std_flat,
233
+ a_sum_flat,
234
+ coef_flat,
235
+ )
236
+ return result.reshape(*batch_dims, n)
237
+
238
+
239
+ def _softsort_fused_rank(
240
+ x_last: Array,
241
+ batch_dims: list[int],
242
+ softness: float | Array,
243
+ mode: SimplexMode,
244
+ descending: bool,
245
+ ) -> Array:
246
+ """Ranks via SoftSort, O(n) memory. x_last should be standardized."""
247
+ n = x_last.shape[-1]
248
+ nums = jnp.arange(1, n + 1, dtype=x_last.dtype)
249
+ x_flat = x_last.reshape(-1, n)
250
+
251
+ def _single(x_row: Array) -> Array:
252
+ """Compute soft ranks for a single row via SoftSort."""
253
+
254
+ def _chunk_fn(x_chunk: Array) -> Array:
255
+ """Compute rank contributions for a chunk of elements."""
256
+ diff = jnp.abs(x_chunk[:, None] - anchors_row[None, :])
257
+ p_chunk = proj_simplex(-diff, axis=-1, softness=softness, mode=mode)
258
+ return jnp.einsum("cn,n->c", p_chunk, nums)
259
+
260
+ anchors_row = jnp.sort(x_row, descending=descending)
261
+ return map_in_chunks(f=_chunk_fn, xs=x_row, chunk_size=128)
262
+
263
+ result = jax.vmap(_single)(x_flat)
264
+ return result.reshape(*batch_dims, n)
265
+
266
+
267
+ def _neuralsort_fused_rank(
268
+ x_last: Array,
269
+ batch_dims: list[int],
270
+ softness: float | Array,
271
+ mode: SimplexMode,
272
+ descending: bool,
273
+ ) -> Array:
274
+ """Ranks via NeuralSort, O(n) memory. x_last should be standardized."""
275
+ n = x_last.shape[-1]
276
+ nums = jnp.arange(1, n + 1, dtype=x_last.dtype)
277
+ row_sums = _neuralsort_a_sum(x_last=x_last, mode=mode, softness=softness)
278
+
279
+ i = jnp.arange(1, n + 1)
280
+ if descending:
281
+ i = i[::-1]
282
+ coef = n + 1 - 2 * i
283
+ coef = jnp.broadcast_to(
284
+ coef.reshape(*(1,) * len(batch_dims), n),
285
+ (*batch_dims, n),
286
+ )
287
+
288
+ x_flat = x_last.reshape(-1, n)
289
+ row_sums_flat = row_sums.reshape(-1, n)
290
+ coef_flat = coef.reshape(-1, n)
291
+
292
+ def _single(
293
+ x_row: Array,
294
+ row_sums_row: Array,
295
+ coef_row: Array,
296
+ ) -> Array:
297
+ """Compute soft ranks for a single row via NeuralSort."""
298
+ coef_and_nums = jnp.stack([coef_row, nums], axis=-1)
299
+
300
+ def _chunk_fn(data_chunk: Array) -> Array:
301
+ """Accumulate rank contributions from a chunk of coefficients."""
302
+ coef_chunk = data_chunk[:, 0]
303
+ nums_chunk = data_chunk[:, 1]
304
+ z_chunk = -(coef_chunk[:, None] * x_row[None, :] + row_sums_row[None, :])
305
+ p_chunk = proj_simplex(z_chunk, axis=-1, softness=softness, mode=mode)
306
+ col_sum = p_chunk.sum(axis=0)
307
+ weighted = (nums_chunk[:, None] * p_chunk).sum(axis=0)
308
+ return jnp.stack([col_sum, weighted])
309
+
310
+ result = reduce_in_chunks(f=_chunk_fn, xs=coef_and_nums, chunk_size=128)
311
+ col_sums = result[0]
312
+ weighted_sums = result[1]
313
+ return weighted_sums / jnp.clip(col_sums, min=1e-10)
314
+
315
+ result = jax.vmap(_single)(x_flat, row_sums_flat, coef_flat)
316
+ return result.reshape(*batch_dims, n)
317
+
318
+
319
+ # ---------------------------------------------------------------------------
320
+ # Public API
321
+ # ---------------------------------------------------------------------------
322
+
323
+
324
+ def argmax(
325
+ x: Array,
326
+ axis: int | None = None,
327
+ keepdims: bool = False,
328
+ softness: float | Array = 0.1,
329
+ mode: Mode = "smooth",
330
+ method: ArgMethod = "softsort",
331
+ standardize: bool = True,
332
+ ot_kwargs: dict | None = None,
333
+ ) -> SoftIndex:
334
+ """Soft argmax returning a SoftIndex (probability distribution).
335
+
336
+ Args:
337
+ x: Input array.
338
+ axis: Axis along which to compute argmax. None flattens first.
339
+ keepdims: If True, keep the reduced dimension as singleton.
340
+ softness: Controls sharpness (> 0).
341
+ mode: Smoothness mode.
342
+ method: Algorithm: ``"softsort"``, ``"neuralsort"``,
343
+ ``"sorting_network"``, or ``"ot"``.
344
+ standardize: If True, standardize input for numerical stability.
345
+ ot_kwargs: Extra kwargs for OT method.
346
+
347
+ Returns:
348
+ SoftIndex of shape ``(..., {1}, ..., [n])``.
349
+ """
350
+ if mode == "hard":
351
+ indices = jnp.argmax(x, axis=axis, keepdims=keepdims)
352
+ num_classes = jnp.size(x, axis=axis)
353
+ return jax.nn.one_hot(indices, num_classes=num_classes, axis=-1)
354
+
355
+ x = ensure_float(x)
356
+ if axis is None:
357
+ num_dims = x.ndim
358
+ x = jnp.ravel(x)
359
+ _axis = 0
360
+ else:
361
+ _axis = canonicalize_axis(axis, x.ndim)
362
+ num_dims = None
363
+
364
+ if standardize:
365
+ x = standardize_and_squash(x, axis=_axis)
366
+
367
+ x_last = jnp.moveaxis(x, _axis, -1)
368
+ *batch_dims, n = x_last.shape
369
+
370
+ if method == "softsort":
371
+ soft_index = proj_simplex(
372
+ x_last,
373
+ axis=-1,
374
+ softness=softness,
375
+ mode=mode,
376
+ )
377
+ elif method == "neuralsort":
378
+ a_sum = _neuralsort_a_sum(x_last, mode=mode, softness=softness)
379
+ z = (n - 1) * x_last - a_sum
380
+ soft_index = proj_simplex(z, axis=-1, softness=softness, mode=mode)
381
+ elif method == "sorting_network":
382
+ soft_index = _sorting_network_argmax_index(
383
+ x_last,
384
+ softness,
385
+ mode,
386
+ standardize=standardize,
387
+ )
388
+ elif method == "ot":
389
+ _proj_tp = _get_proj_transport_polytope()
390
+ anchors = jnp.array([0.0, 1.0], dtype=x.dtype)
391
+ anchors = jnp.broadcast_to(anchors, (*batch_dims, 2))
392
+ cost = (x_last[..., :, None] - anchors[..., None, :]) ** 2
393
+ mu = jnp.ones((n,), dtype=x.dtype) / n
394
+ nu = jnp.array([(n - 1) / n, 1 / n], dtype=x.dtype)
395
+ if ot_kwargs is None:
396
+ ot_kwargs = {}
397
+ out = _proj_tp(
398
+ cost=cost,
399
+ mu=mu,
400
+ nu=nu,
401
+ softness=softness,
402
+ mode=mode,
403
+ **ot_kwargs,
404
+ )
405
+ soft_index = out[..., :, 1]
406
+ else:
407
+ msg = f"Invalid method: {method!r}"
408
+ raise ValueError(msg)
409
+
410
+ if keepdims:
411
+ if num_dims is not None:
412
+ soft_index = soft_index.reshape(*(1,) * num_dims, n)
413
+ else:
414
+ soft_index = jnp.expand_dims(soft_index, axis=_axis)
415
+ return soft_index
416
+
417
+
418
+ def max(
419
+ x: Array,
420
+ axis: int | None = None,
421
+ keepdims: bool = False,
422
+ softness: float | Array = 0.1,
423
+ mode: Mode = "smooth",
424
+ method: SortMethod = "softsort",
425
+ standardize: bool = True,
426
+ ot_kwargs: dict | None = None,
427
+ gated_grad: bool = True,
428
+ ) -> Array:
429
+ """Soft max via argmax + take_along_axis.
430
+
431
+ For ``sorting_network`` method, uses sort + take first element.
432
+
433
+ Args:
434
+ x: Input array.
435
+ axis: Axis along which to compute max.
436
+ keepdims: If True, keep reduced dimension.
437
+ softness: Controls sharpness (> 0).
438
+ mode: Smoothness mode.
439
+ method: Algorithm (see :func:`argmax` and :func:`sort`).
440
+ standardize: If True, standardize input.
441
+ ot_kwargs: Extra kwargs for OT method.
442
+ gated_grad: If False, stop gradient through soft index.
443
+
444
+ Returns:
445
+ Soft maximum value(s).
446
+ """
447
+ if mode == "hard":
448
+ return jnp.max(x, axis=axis, keepdims=keepdims)
449
+
450
+ if axis is None:
451
+ num_dims = x.ndim
452
+ x = jnp.ravel(x)
453
+ _axis = 0
454
+ else:
455
+ _axis = canonicalize_axis(axis, x.ndim)
456
+ num_dims = None
457
+
458
+ sort_methods: set[str] = {"sorting_network", "fast_soft_sort", "smooth_sort"}
459
+ if method in sort_methods:
460
+ soft_sorted = sort(
461
+ x,
462
+ axis=_axis,
463
+ descending=True,
464
+ softness=softness,
465
+ standardize=standardize,
466
+ mode=mode,
467
+ method=method,
468
+ )
469
+ max_val = jnp.take(soft_sorted, indices=0, axis=_axis)
470
+ if num_dims is not None and keepdims:
471
+ max_val = max_val.reshape(*(1,) * num_dims)
472
+ elif keepdims:
473
+ max_val = jnp.expand_dims(max_val, axis=_axis)
474
+ else:
475
+ # method is one of ArgMethod: softsort, neuralsort, sorting_network, ot
476
+ arg_method: ArgMethod = method # type: ignore[assignment]
477
+ soft_index = argmax(
478
+ x,
479
+ axis=_axis,
480
+ keepdims=True,
481
+ softness=softness,
482
+ mode=mode,
483
+ method=arg_method,
484
+ standardize=standardize,
485
+ ot_kwargs=ot_kwargs,
486
+ )
487
+ if not gated_grad:
488
+ soft_index = jax.lax.stop_gradient(soft_index)
489
+ max_val = take_along_axis(x, soft_index, axis=_axis)
490
+ if num_dims is not None:
491
+ max_val = max_val.reshape(*(1,) * num_dims)
492
+ if not keepdims:
493
+ max_val = jnp.squeeze(max_val, axis=axis)
494
+ return max_val
495
+
496
+
497
+ def argmin(
498
+ x: Array,
499
+ axis: int | None = None,
500
+ keepdims: bool = False,
501
+ softness: float | Array = 0.1,
502
+ mode: Mode = "smooth",
503
+ method: ArgMethod = "softsort",
504
+ standardize: bool = True,
505
+ ot_kwargs: dict | None = None,
506
+ ) -> SoftIndex:
507
+ """Soft argmin: :func:`argmax` on ``-x``."""
508
+ return argmax(
509
+ -x,
510
+ axis=axis,
511
+ mode=mode,
512
+ method=method,
513
+ softness=softness,
514
+ keepdims=keepdims,
515
+ standardize=standardize,
516
+ ot_kwargs=ot_kwargs,
517
+ )
518
+
519
+
520
+ def min(
521
+ x: Array,
522
+ axis: int | None = None,
523
+ keepdims: bool = False,
524
+ softness: float | Array = 0.1,
525
+ mode: Mode = "smooth",
526
+ method: SortMethod = "softsort",
527
+ standardize: bool = True,
528
+ ot_kwargs: dict | None = None,
529
+ gated_grad: bool = True,
530
+ ) -> Array:
531
+ """Soft min: ``-max(-x)``."""
532
+ return -max(
533
+ -x,
534
+ axis=axis,
535
+ softness=softness,
536
+ mode=mode,
537
+ method=method,
538
+ keepdims=keepdims,
539
+ standardize=standardize,
540
+ ot_kwargs=ot_kwargs,
541
+ gated_grad=gated_grad,
542
+ )
543
+
544
+
545
+ def argsort(
546
+ x: Array,
547
+ axis: int | None = None,
548
+ descending: bool = False,
549
+ softness: float | Array = 0.1,
550
+ mode: Mode = "smooth",
551
+ method: ArgMethod = "neuralsort",
552
+ standardize: bool = True,
553
+ ot_kwargs: dict | None = None,
554
+ ) -> SoftIndex:
555
+ """Soft argsort returning a soft permutation matrix.
556
+
557
+ Output shape is ``(..., n, ..., [n])`` where the last dimension
558
+ is the probability distribution over original elements.
559
+
560
+ Args:
561
+ x: Input array.
562
+ axis: Axis along which to argsort. None flattens first.
563
+ descending: If True, sort descending.
564
+ softness: Controls sharpness (> 0).
565
+ mode: Smoothness mode.
566
+ method: Algorithm.
567
+ standardize: If True, standardize input.
568
+ ot_kwargs: Extra kwargs for OT method.
569
+
570
+ Returns:
571
+ SoftIndex permutation matrix.
572
+ """
573
+ if mode == "hard":
574
+ indices = jnp.argsort(x, axis=axis, descending=descending)
575
+ num_classes = jnp.size(x, axis=axis)
576
+ return jax.nn.one_hot(indices, num_classes=num_classes, axis=-1)
577
+
578
+ x = ensure_float(x)
579
+ x, axis = normalize_axis_argument(x, axis)
580
+
581
+ if standardize:
582
+ x = standardize_and_squash(x, axis=axis)
583
+
584
+ x_last = jnp.moveaxis(x, axis, -1)
585
+ *batch_dims, n = x_last.shape
586
+
587
+ if method == "softsort":
588
+ anchors = jnp.sort(x_last, axis=-1, descending=descending)
589
+ diff = jnp.abs(anchors[..., :, None] - x_last[..., None, :])
590
+ soft_index = proj_simplex(-diff, axis=-1, softness=softness, mode=mode)
591
+ elif method == "neuralsort":
592
+ a_sum = _neuralsort_a_sum(x_last, mode=mode, softness=softness)
593
+ i = jnp.arange(1, n + 1)
594
+ if descending:
595
+ i = i[::-1]
596
+ coef = n + 1 - 2 * i
597
+ coef = jnp.broadcast_to(coef, (*batch_dims, n))
598
+ z = -(coef[..., :, None] * x_last[..., None, :] + a_sum[..., None, :])
599
+ soft_index = proj_simplex(z, axis=-1, softness=softness, mode=mode)
600
+ elif method == "sorting_network":
601
+ soft_index = _sorting_network_permutation(
602
+ x_last,
603
+ softness,
604
+ mode,
605
+ descending=descending,
606
+ standardized=standardize,
607
+ )
608
+ elif method == "ot":
609
+ _proj_tp = _get_proj_transport_polytope()
610
+ anchors = jnp.linspace(0, n, n, dtype=x.dtype) / n
611
+ if descending:
612
+ anchors = anchors[::-1]
613
+ anchors = jnp.broadcast_to(anchors, (*batch_dims, n))
614
+ cost = (x_last[..., :, None] - anchors[..., None, :]) ** 2
615
+ mu = jnp.ones((n,), dtype=x.dtype) / n
616
+ nu = jnp.ones((n,), dtype=x.dtype) / n
617
+ if ot_kwargs is None:
618
+ ot_kwargs = {}
619
+ out = _proj_tp(
620
+ cost=cost,
621
+ mu=mu,
622
+ nu=nu,
623
+ softness=softness,
624
+ mode=mode,
625
+ **ot_kwargs,
626
+ )
627
+ soft_index = jnp.swapaxes(out, -2, -1)
628
+ else:
629
+ msg = f"Invalid method: {method!r}"
630
+ raise ValueError(msg)
631
+
632
+ return jnp.moveaxis(soft_index, -2, axis)
633
+
634
+
635
+ def sort(
636
+ x: Array,
637
+ axis: int | None = None,
638
+ descending: bool = False,
639
+ softness: float | Array = 0.1,
640
+ mode: Mode = "smooth",
641
+ method: SortMethod = "neuralsort",
642
+ standardize: bool = True,
643
+ ot_kwargs: dict | None = None,
644
+ gated_grad: bool = True,
645
+ ) -> Array:
646
+ """Soft sort returning sorted values.
647
+
648
+ Args:
649
+ x: Input array.
650
+ axis: Axis along which to sort. None flattens first.
651
+ descending: If True, sort descending.
652
+ softness: Controls sharpness (> 0).
653
+ mode: Smoothness mode.
654
+ method: Algorithm.
655
+ standardize: If True, standardize input.
656
+ ot_kwargs: Extra kwargs for OT method.
657
+ gated_grad: If False, stop gradient through soft index.
658
+
659
+ Returns:
660
+ Soft-sorted values.
661
+ """
662
+ if mode == "hard":
663
+ return jnp.sort(x, axis=axis, descending=descending)
664
+
665
+ x = ensure_float(x)
666
+ x, axis = normalize_axis_argument(x, axis)
667
+
668
+ if method == "sorting_network":
669
+ if standardize:
670
+ x, mean, std = standardize_and_squash(
671
+ x,
672
+ axis=axis,
673
+ return_mean_std=True,
674
+ )
675
+ x_last = jnp.moveaxis(x, axis, -1)
676
+ soft_values = sort_via_sorting_network(
677
+ x_last,
678
+ softness=softness,
679
+ mode=mode,
680
+ descending=descending,
681
+ standardized=standardize,
682
+ )
683
+ soft_values = jnp.moveaxis(soft_values, -1, axis)
684
+ if standardize:
685
+ soft_values = unsquash_and_destandardize(
686
+ y=soft_values,
687
+ mean=mean,
688
+ std=std,
689
+ )
690
+ elif method == "fast_soft_sort":
691
+ _proj_perm = _get_proj_permutahedron()
692
+ if standardize:
693
+ x, mean, std = standardize_and_squash(
694
+ x,
695
+ axis=axis,
696
+ return_mean_std=True,
697
+ )
698
+ x_last = jnp.moveaxis(x, axis, -1)
699
+ *batch_dims, n = x_last.shape
700
+ w = x_last
701
+ anchors = jnp.arange(n, dtype=x.dtype) / jnp.maximum((n - 1), 1)
702
+ anchors = jnp.broadcast_to(anchors, (*batch_dims, n))
703
+ soft_values = _proj_perm(anchors, w, softness=softness, mode=mode)
704
+ soft_values = jnp.moveaxis(soft_values, -1, axis)
705
+ if descending:
706
+ soft_values = jnp.flip(soft_values, axis=axis)
707
+ if standardize:
708
+ soft_values = unsquash_and_destandardize(
709
+ y=soft_values,
710
+ mean=mean,
711
+ std=std,
712
+ )
713
+ elif method == "smooth_sort":
714
+ _proj_perm_ss = _get_proj_permutahedron_smooth_sort()
715
+ if mode != "smooth":
716
+ msg = f"smooth_sort only supports mode='smooth', got mode={mode!r}"
717
+ raise ValueError(msg)
718
+ x_last = jnp.moveaxis(x, axis, -1)
719
+ *batch_dims, n = x_last.shape
720
+ w = x_last
721
+ anchors = jnp.arange(n, dtype=x.dtype) / jnp.maximum((n - 1), 1)
722
+ anchors = jnp.broadcast_to(anchors, (*batch_dims, n))
723
+ soft_values = _proj_perm_ss(anchors, w, softness=softness)
724
+ soft_values = jnp.moveaxis(soft_values, -1, axis)
725
+ if descending:
726
+ soft_values = jnp.flip(soft_values, axis=axis)
727
+ elif method == "softsort":
728
+ x_last = jnp.moveaxis(x, axis, -1)
729
+ *batch_dims, n = x_last.shape
730
+ soft_values = _softsort_fused_sort(
731
+ x_last,
732
+ batch_dims,
733
+ softness,
734
+ mode,
735
+ descending,
736
+ standardize,
737
+ gated_grad,
738
+ )
739
+ soft_values = jnp.moveaxis(soft_values, -1, axis)
740
+ elif method == "neuralsort":
741
+ x_last = jnp.moveaxis(x, axis, -1)
742
+ *batch_dims, n = x_last.shape
743
+ soft_values = _neuralsort_fused_sort(
744
+ x_last,
745
+ batch_dims,
746
+ softness,
747
+ mode,
748
+ descending,
749
+ standardize,
750
+ gated_grad,
751
+ )
752
+ soft_values = jnp.moveaxis(soft_values, -1, axis)
753
+ else:
754
+ # Fallback: argsort + take_along_axis (for ot method)
755
+ arg_method: ArgMethod = method # type: ignore[assignment]
756
+ soft_index = argsort(
757
+ x,
758
+ axis=axis,
759
+ descending=descending,
760
+ softness=softness,
761
+ mode=mode,
762
+ method=arg_method,
763
+ standardize=standardize,
764
+ ot_kwargs=ot_kwargs,
765
+ )
766
+ if not gated_grad:
767
+ soft_index = jax.lax.stop_gradient(soft_index)
768
+ soft_values = take_along_axis(x, soft_index, axis=axis)
769
+ return soft_values
770
+
771
+
772
+ def rank(
773
+ x: Array,
774
+ axis: int | None = None,
775
+ descending: bool = False,
776
+ softness: float | Array = 0.1,
777
+ mode: Mode = "smooth",
778
+ method: RankMethod = "softsort",
779
+ standardize: bool = True,
780
+ ) -> Array:
781
+ """Soft fractional ranking.
782
+
783
+ Returns continuous ranks in [1, n] where 1 is the smallest.
784
+
785
+ Args:
786
+ x: Input array.
787
+ axis: Axis along which to rank.
788
+ descending: If True, rank 1 = largest.
789
+ softness: Controls sharpness (> 0).
790
+ mode: Smoothness mode.
791
+ method: ``"softsort"`` or ``"neuralsort"``.
792
+ standardize: If True, standardize input.
793
+
794
+ Returns:
795
+ Continuous ranks.
796
+ """
797
+ if mode == "hard":
798
+ indices = jnp.argsort(x, axis=axis, descending=descending)
799
+ ranks = jnp.empty_like(indices, dtype=jnp.float32)
800
+ n = jnp.size(x, axis=axis)
801
+ nums = jnp.arange(1, n + 1)
802
+ # Scatter ranks back to original positions
803
+ if axis is None:
804
+ ranks = ranks.ravel()
805
+ ranks = ranks.at[indices.ravel()].set(nums.astype(jnp.float32))
806
+ return ranks.reshape(x.shape)
807
+ ranks = jnp.take_along_axis(
808
+ jnp.broadcast_to(
809
+ jnp.expand_dims(nums, tuple(range(x.ndim - 1))),
810
+ x.shape,
811
+ ).astype(jnp.float32),
812
+ jnp.argsort(indices, axis=axis),
813
+ axis=axis,
814
+ )
815
+ return ranks
816
+
817
+ x = ensure_float(x)
818
+ x, axis = normalize_axis_argument(x, axis)
819
+
820
+ if standardize:
821
+ x = standardize_and_squash(x, axis=axis)
822
+
823
+ x_last = jnp.moveaxis(x, axis, -1)
824
+ *batch_dims, n = x_last.shape
825
+
826
+ if method == "softsort":
827
+ result = _softsort_fused_rank(
828
+ x_last,
829
+ batch_dims,
830
+ softness,
831
+ mode,
832
+ descending,
833
+ )
834
+ elif method == "neuralsort":
835
+ result = _neuralsort_fused_rank(
836
+ x_last,
837
+ batch_dims,
838
+ softness,
839
+ mode,
840
+ descending,
841
+ )
842
+ else:
843
+ msg = f"Invalid method for rank: {method!r}"
844
+ raise ValueError(msg)
845
+
846
+ return jnp.moveaxis(result, -1, axis)
847
+
848
+
849
+ def top_k(
850
+ x: Array,
851
+ k: int,
852
+ axis: int = -1,
853
+ softness: float | Array = 0.1,
854
+ mode: Mode = "smooth",
855
+ method: SortMethod = "neuralsort",
856
+ standardize: bool = True,
857
+ ot_kwargs: dict | None = None,
858
+ gated_grad: bool = True,
859
+ ) -> tuple[Array, SoftIndex | None]:
860
+ """Soft top-k selection.
861
+
862
+ Returns the k largest values and their soft indices.
863
+
864
+ Args:
865
+ x: Input array.
866
+ k: Number of top elements.
867
+ axis: Axis along which to select. Default -1 (last axis).
868
+ softness: Controls sharpness (> 0).
869
+ mode: Smoothness mode.
870
+ method: Sorting algorithm. Default ``"neuralsort"``.
871
+ standardize: If True, standardize input.
872
+ ot_kwargs: Extra keyword arguments for OT-based methods.
873
+ gated_grad: If False, stop gradient through soft index.
874
+
875
+ Returns:
876
+ Tuple of (values, soft_indices) where values has shape
877
+ ``(..., k, ...)`` and soft_indices has shape
878
+ ``(..., k, ..., [n])``. soft_indices may be None for
879
+ methods that only return values (fast_soft_sort, sorting_network).
880
+ """
881
+ if mode == "hard":
882
+ indices = jnp.argsort(x, axis=axis, descending=True)
883
+ indices_k = jnp.take(indices, jnp.arange(k), axis=axis)
884
+ values = jnp.take_along_axis(x, indices_k, axis=axis)
885
+ soft_indices = jax.nn.one_hot(indices_k, x.shape[axis], axis=-1)
886
+ return values, soft_indices
887
+
888
+ # Methods that only return sorted values (no indices)
889
+ if method in ("fast_soft_sort", "sorting_network"):
890
+ sorted_vals = sort(
891
+ x,
892
+ axis=axis,
893
+ descending=True,
894
+ softness=softness,
895
+ mode=mode,
896
+ method=method,
897
+ standardize=standardize,
898
+ ot_kwargs=ot_kwargs,
899
+ )
900
+ values = jnp.take(sorted_vals, jnp.arange(k), axis=axis)
901
+ return values, None
902
+
903
+ # Methods that produce soft indices
904
+ arg_method: ArgMethod = method # type: ignore[assignment]
905
+ soft_index = argsort(
906
+ x,
907
+ axis=axis,
908
+ descending=True,
909
+ softness=softness,
910
+ mode=mode,
911
+ method=arg_method,
912
+ standardize=standardize,
913
+ ot_kwargs=ot_kwargs,
914
+ )
915
+ if not gated_grad:
916
+ soft_index = jax.lax.stop_gradient(soft_index)
917
+
918
+ # ``argsort`` appends the probability-distribution axis at index -1 of
919
+ # ``soft_index``. The rank axis (where we slice the top k entries) is
920
+ # therefore at position ``axis`` if ``axis >= 0`` — the append only grew
921
+ # the tail — but at ``axis - 1`` for negative ``axis``, since the new
922
+ # ``[n]`` dim slid every negatively-indexed axis one slot to the left.
923
+ rank_axis = axis if axis >= 0 else axis - 1
924
+ soft_index_k = jnp.take(soft_index, jnp.arange(k), axis=rank_axis)
925
+ values = take_along_axis(x, soft_index_k, axis=axis)
926
+ return values, soft_index_k