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.
- diffbio/__init__.py +39 -0
- diffbio/configs.py +75 -0
- diffbio/constants.py +204 -0
- diffbio/core/__init__.py +127 -0
- diffbio/core/base_operators.py +612 -0
- diffbio/core/data_types.py +260 -0
- diffbio/core/gnn_components.py +629 -0
- diffbio/core/graph_utils.py +149 -0
- diffbio/core/neural_components.py +270 -0
- diffbio/core/optimal_transport.py +133 -0
- diffbio/core/soft_ops/__init__.py +216 -0
- diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
- diffbio/core/soft_ops/_projections_simplex.py +240 -0
- diffbio/core/soft_ops/_projections_transport.py +508 -0
- diffbio/core/soft_ops/_sorting_network.py +204 -0
- diffbio/core/soft_ops/_types.py +15 -0
- diffbio/core/soft_ops/_utils.py +342 -0
- diffbio/core/soft_ops/autograd_safe.py +120 -0
- diffbio/core/soft_ops/comparison.py +235 -0
- diffbio/core/soft_ops/elementwise.py +309 -0
- diffbio/core/soft_ops/logical.py +146 -0
- diffbio/core/soft_ops/quantile.py +376 -0
- diffbio/core/soft_ops/selection.py +236 -0
- diffbio/core/soft_ops/sorting.py +926 -0
- diffbio/core/soft_ops/straight_through.py +261 -0
- diffbio/core/uncertainty.py +279 -0
- diffbio/evaluation/__init__.py +42 -0
- diffbio/evaluation/adapters.py +409 -0
- diffbio/evaluation/graders.py +223 -0
- diffbio/evaluation/problem.py +157 -0
- diffbio/evaluation/runner.py +277 -0
- diffbio/losses/__init__.py +59 -0
- diffbio/losses/alignment_losses.py +222 -0
- diffbio/losses/biological_regularization.py +288 -0
- diffbio/losses/metric_losses.py +139 -0
- diffbio/losses/singlecell_losses.py +387 -0
- diffbio/losses/statistical_losses.py +345 -0
- diffbio/operators/__init__.py +60 -0
- diffbio/operators/_count_vae.py +197 -0
- diffbio/operators/_loss_balancing.py +65 -0
- diffbio/operators/_masked_gene_transformer.py +118 -0
- diffbio/operators/_transformer_validation.py +50 -0
- diffbio/operators/alignment/__init__.py +51 -0
- diffbio/operators/alignment/profile_hmm.py +350 -0
- diffbio/operators/alignment/scoring.py +127 -0
- diffbio/operators/alignment/smith_waterman.py +261 -0
- diffbio/operators/alignment/soft_msa.py +419 -0
- diffbio/operators/assembly/__init__.py +27 -0
- diffbio/operators/assembly/gnn_assembly.py +252 -0
- diffbio/operators/assembly/metagenomic_binning.py +296 -0
- diffbio/operators/crispr/__init__.py +17 -0
- diffbio/operators/crispr/guide_scoring.py +269 -0
- diffbio/operators/drug_discovery/__init__.py +133 -0
- diffbio/operators/drug_discovery/_graph_utils.py +142 -0
- diffbio/operators/drug_discovery/admet_predictor.py +285 -0
- diffbio/operators/drug_discovery/attentive_fp.py +411 -0
- diffbio/operators/drug_discovery/dti.py +261 -0
- diffbio/operators/drug_discovery/fingerprint.py +490 -0
- diffbio/operators/drug_discovery/maccs_keys.py +267 -0
- diffbio/operators/drug_discovery/message_passing.py +200 -0
- diffbio/operators/drug_discovery/primitives.py +242 -0
- diffbio/operators/drug_discovery/property_predictor.py +163 -0
- diffbio/operators/drug_discovery/similarity.py +193 -0
- diffbio/operators/epigenomics/__init__.py +35 -0
- diffbio/operators/epigenomics/chromatin_state.py +491 -0
- diffbio/operators/epigenomics/contextual.py +288 -0
- diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
- diffbio/operators/epigenomics/peak_calling.py +555 -0
- diffbio/operators/foundation_models/__init__.py +119 -0
- diffbio/operators/foundation_models/adapters.py +114 -0
- diffbio/operators/foundation_models/contracts.py +245 -0
- diffbio/operators/foundation_models/embedding_probe.py +83 -0
- diffbio/operators/foundation_models/experimental.py +128 -0
- diffbio/operators/foundation_models/foundation_model.py +332 -0
- diffbio/operators/foundation_models/frozen.py +59 -0
- diffbio/operators/foundation_models/precomputed.py +270 -0
- diffbio/operators/foundation_models/transformer_encoder.py +564 -0
- diffbio/operators/mapping/__init__.py +17 -0
- diffbio/operators/mapping/neural_mapper.py +493 -0
- diffbio/operators/metabolomics/__init__.py +39 -0
- diffbio/operators/metabolomics/spectral_similarity.py +315 -0
- diffbio/operators/molecular_dynamics/__init__.py +51 -0
- diffbio/operators/molecular_dynamics/force_field.py +265 -0
- diffbio/operators/molecular_dynamics/integrator.py +304 -0
- diffbio/operators/molecular_dynamics/primitives.py +115 -0
- diffbio/operators/multiomics/__init__.py +38 -0
- diffbio/operators/multiomics/hic_contact.py +377 -0
- diffbio/operators/multiomics/multiomics_vae.py +325 -0
- diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
- diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
- diffbio/operators/normalization/__init__.py +42 -0
- diffbio/operators/normalization/embedding.py +222 -0
- diffbio/operators/normalization/phate.py +400 -0
- diffbio/operators/normalization/umap.py +261 -0
- diffbio/operators/normalization/vae_normalizer.py +258 -0
- diffbio/operators/population/__init__.py +17 -0
- diffbio/operators/population/ancestry_estimation.py +274 -0
- diffbio/operators/preprocessing/__init__.py +76 -0
- diffbio/operators/preprocessing/adapter_removal.py +311 -0
- diffbio/operators/preprocessing/duplicate_filter.py +317 -0
- diffbio/operators/preprocessing/error_correction.py +287 -0
- diffbio/operators/protein/__init__.py +31 -0
- diffbio/operators/protein/secondary_structure.py +509 -0
- diffbio/operators/quality_filter.py +128 -0
- diffbio/operators/rna_structure/__init__.py +35 -0
- diffbio/operators/rna_structure/rna_folding.py +509 -0
- diffbio/operators/rnaseq/__init__.py +23 -0
- diffbio/operators/rnaseq/motif_discovery.py +251 -0
- diffbio/operators/rnaseq/splicing_psi.py +216 -0
- diffbio/operators/singlecell/__init__.py +193 -0
- diffbio/operators/singlecell/ambient_removal.py +333 -0
- diffbio/operators/singlecell/archetypes.py +191 -0
- diffbio/operators/singlecell/batch_correction.py +288 -0
- diffbio/operators/singlecell/cell_annotation.py +519 -0
- diffbio/operators/singlecell/communication.py +704 -0
- diffbio/operators/singlecell/differential_distribution.py +243 -0
- diffbio/operators/singlecell/doublet_detection.py +657 -0
- diffbio/operators/singlecell/downsampling.py +166 -0
- diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
- diffbio/operators/singlecell/grn_inference.py +336 -0
- diffbio/operators/singlecell/imputation.py +429 -0
- diffbio/operators/singlecell/knockdown_filter.py +176 -0
- diffbio/operators/singlecell/ot_trajectory.py +277 -0
- diffbio/operators/singlecell/simulation.py +444 -0
- diffbio/operators/singlecell/sindy_grn.py +247 -0
- diffbio/operators/singlecell/soft_clustering.py +211 -0
- diffbio/operators/singlecell/spatial_domains.py +677 -0
- diffbio/operators/singlecell/switch_de.py +184 -0
- diffbio/operators/singlecell/trajectory.py +447 -0
- diffbio/operators/singlecell/velocity.py +361 -0
- diffbio/operators/statistical/__init__.py +35 -0
- diffbio/operators/statistical/em_quantification.py +260 -0
- diffbio/operators/statistical/hmm.py +234 -0
- diffbio/operators/statistical/nb_glm.py +272 -0
- diffbio/operators/variant/__init__.py +64 -0
- diffbio/operators/variant/classifier.py +333 -0
- diffbio/operators/variant/cnn_classifier.py +255 -0
- diffbio/operators/variant/cnv_segmentation.py +678 -0
- diffbio/operators/variant/deepvariant_pileup.py +426 -0
- diffbio/operators/variant/pileup.py +240 -0
- diffbio/operators/variant/quality_recalibration.py +274 -0
- diffbio/pipelines/__init__.py +65 -0
- diffbio/pipelines/differential_expression.py +279 -0
- diffbio/pipelines/enhanced_variant_calling.py +326 -0
- diffbio/pipelines/perturbation.py +407 -0
- diffbio/pipelines/preprocessing.py +267 -0
- diffbio/pipelines/single_cell.py +366 -0
- diffbio/pipelines/variant_calling.py +490 -0
- diffbio/samplers/__init__.py +9 -0
- diffbio/samplers/perturbation_sampler.py +142 -0
- diffbio/sequences/__init__.py +34 -0
- diffbio/sequences/dna.py +239 -0
- diffbio/sources/__init__.py +149 -0
- diffbio/sources/_anndata_shared.py +89 -0
- diffbio/sources/_batch_iteration.py +37 -0
- diffbio/sources/_benchmark_source.py +152 -0
- diffbio/sources/_indexed_batch_source.py +38 -0
- diffbio/sources/_utils.py +45 -0
- diffbio/sources/anndata_interop.py +387 -0
- diffbio/sources/anndata_source.py +361 -0
- diffbio/sources/archive_ii.py +174 -0
- diffbio/sources/balifam.py +207 -0
- diffbio/sources/bam.py +265 -0
- diffbio/sources/bengrn_ground_truth.py +306 -0
- diffbio/sources/contextual_epigenomics.py +242 -0
- diffbio/sources/dti.py +359 -0
- diffbio/sources/embeddings.py +203 -0
- diffbio/sources/encode_peaks.py +223 -0
- diffbio/sources/fasta.py +226 -0
- diffbio/sources/immune_human.py +172 -0
- diffbio/sources/indexed_embeddings.py +128 -0
- diffbio/sources/indexed_view.py +191 -0
- diffbio/sources/molnet.py +493 -0
- diffbio/sources/multiomics.py +279 -0
- diffbio/sources/pancreas.py +108 -0
- diffbio/sources/perturbation/__init__.py +69 -0
- diffbio/sources/perturbation/_types.py +51 -0
- diffbio/sources/perturbation/_utils.py +125 -0
- diffbio/sources/perturbation/concat_source.py +115 -0
- diffbio/sources/perturbation/control_mapping.py +215 -0
- diffbio/sources/perturbation/experiment_config.py +261 -0
- diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
- diffbio/sources/perturbation/output_space.py +52 -0
- diffbio/sources/perturbation/perturbation_source.py +513 -0
- diffbio/sources/seqfish.py +145 -0
- diffbio/sources/sequence_foundation.py +68 -0
- diffbio/sources/singlecell_foundation.py +68 -0
- diffbio/splitters/__init__.py +63 -0
- diffbio/splitters/base.py +251 -0
- diffbio/splitters/molecular.py +330 -0
- diffbio/splitters/perturbation.py +199 -0
- diffbio/splitters/random.py +217 -0
- diffbio/splitters/sequence.py +201 -0
- diffbio/utils/__init__.py +55 -0
- diffbio/utils/dependency_runtime.py +115 -0
- diffbio/utils/nn_utils.py +157 -0
- diffbio/utils/quality.py +45 -0
- diffbio/utils/training.py +585 -0
- diffbio-0.1.0.dist-info/METADATA +480 -0
- diffbio-0.1.0.dist-info/RECORD +202 -0
- diffbio-0.1.0.dist-info/WHEEL +4 -0
- 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
|