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,508 @@
|
|
|
1
|
+
"""Differentiable projection onto the transport polytope.
|
|
2
|
+
|
|
3
|
+
Projects a cost matrix onto the transport polytope between two marginal
|
|
4
|
+
distributions using regularized optimal transport. Multiple regularizers
|
|
5
|
+
control the smoothness of the resulting gradient:
|
|
6
|
+
|
|
7
|
+
- **smooth** (C-infinity): Entropic/softmax regularizer. Solved via
|
|
8
|
+
Sinkhorn or L-BFGS on the dual.
|
|
9
|
+
- **c0** (continuous): Euclidean/L2 regularizer (p=2 p-norm). Solved
|
|
10
|
+
via L-BFGS.
|
|
11
|
+
- **c1** (once differentiable): p=3/2 p-norm regularizer. Solved via
|
|
12
|
+
L-BFGS.
|
|
13
|
+
- **c2** (twice differentiable): p=4/3 p-norm regularizer. Solved via
|
|
14
|
+
L-BFGS.
|
|
15
|
+
|
|
16
|
+
All mathematical implementations are preserved exactly and support JAX
|
|
17
|
+
autodiff via implicit differentiation or recursive checkpointing.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
import jax
|
|
25
|
+
import jax.numpy as jnp
|
|
26
|
+
from jax import Array
|
|
27
|
+
|
|
28
|
+
from diffbio.core.soft_ops._utils import validate_softness
|
|
29
|
+
|
|
30
|
+
# -- Optional dependency: optimistix (L-BFGS solver) ----------------------
|
|
31
|
+
try:
|
|
32
|
+
import optimistix as optx
|
|
33
|
+
|
|
34
|
+
HAS_OPTIMISTIX = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
HAS_OPTIMISTIX = False
|
|
37
|
+
|
|
38
|
+
# -- Optional dependency: lineax (linear solvers) -------------------------
|
|
39
|
+
try:
|
|
40
|
+
import lineax as lx
|
|
41
|
+
|
|
42
|
+
HAS_LINEAX = True
|
|
43
|
+
except ImportError:
|
|
44
|
+
HAS_LINEAX = False
|
|
45
|
+
|
|
46
|
+
# -- Optional dependency: OTT-JAX (Sinkhorn solver) -----------------------
|
|
47
|
+
try:
|
|
48
|
+
from ott.geometry import geometry
|
|
49
|
+
from ott.problems.linear import linear_problem
|
|
50
|
+
from ott.solvers.linear import (
|
|
51
|
+
implicit_differentiation as idiff,
|
|
52
|
+
sinkhorn,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
HAS_OTT = True
|
|
56
|
+
except ImportError:
|
|
57
|
+
HAS_OTT = False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _transport_solver_dtype(dtype: jnp.dtype) -> jnp.dtype:
|
|
61
|
+
"""Choose the highest-precision solver dtype supported by the active JAX config."""
|
|
62
|
+
return jnp.float64 if jax.config.jax_enable_x64 else dtype
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _promote_transport_inputs(
|
|
66
|
+
C: jax.Array,
|
|
67
|
+
mu: jax.Array,
|
|
68
|
+
nu: jax.Array,
|
|
69
|
+
scalar: float | Array,
|
|
70
|
+
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
|
|
71
|
+
"""Promote OT solver inputs without requesting unsupported float64 precision."""
|
|
72
|
+
target_dtype = _transport_solver_dtype(C.dtype)
|
|
73
|
+
C = C.astype(target_dtype)
|
|
74
|
+
mu = mu.astype(target_dtype)
|
|
75
|
+
nu = nu.astype(target_dtype)
|
|
76
|
+
return C, mu, nu, jnp.asarray(scalar, dtype=target_dtype)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ----------------------------------------------------------------------- #
|
|
80
|
+
# Entropic projection via Sinkhorn (requires OTT-JAX)
|
|
81
|
+
# ----------------------------------------------------------------------- #
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _proj_transport_polytope_entropic_sinkhorn(
|
|
85
|
+
C: jax.Array,
|
|
86
|
+
mu: jax.Array,
|
|
87
|
+
nu: jax.Array,
|
|
88
|
+
tol: float = 1e-6,
|
|
89
|
+
max_iter: int = 1000,
|
|
90
|
+
epsilon: float | Array = 1.0,
|
|
91
|
+
) -> jax.Array:
|
|
92
|
+
"""Solve entropic OT via Sinkhorn with implicit differentiation.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
C: Cost matrix of shape ``(n, m)``.
|
|
96
|
+
mu: Source marginal of shape ``(n,)``.
|
|
97
|
+
nu: Target marginal of shape ``(m,)``.
|
|
98
|
+
tol: Convergence tolerance.
|
|
99
|
+
max_iter: Maximum Sinkhorn iterations.
|
|
100
|
+
epsilon: Entropic regularization strength.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Transport plan of shape ``(n, m)``.
|
|
104
|
+
"""
|
|
105
|
+
if not HAS_OTT:
|
|
106
|
+
msg = (
|
|
107
|
+
"OTT-JAX is required for Sinkhorn-based entropic "
|
|
108
|
+
"projection. Install it with: uv pip install ott-jax"
|
|
109
|
+
)
|
|
110
|
+
raise ImportError(msg)
|
|
111
|
+
|
|
112
|
+
orig_dtype = C.dtype
|
|
113
|
+
C, mu, nu, epsilon = _promote_transport_inputs(C, mu, nu, epsilon)
|
|
114
|
+
|
|
115
|
+
# Avoid exact zeros (helps implicit differentiation a lot)
|
|
116
|
+
tiny = 1e-12
|
|
117
|
+
mu = jnp.clip(mu, tiny)
|
|
118
|
+
mu = mu / jnp.sum(mu)
|
|
119
|
+
nu = jnp.clip(nu, tiny)
|
|
120
|
+
nu = nu / jnp.sum(nu)
|
|
121
|
+
|
|
122
|
+
geom = geometry.Geometry(cost_matrix=C, epsilon=epsilon) # pyright: ignore[reportArgumentType]
|
|
123
|
+
prob = linear_problem.LinearProblem(geom, a=mu, b=nu)
|
|
124
|
+
|
|
125
|
+
implicit = idiff.ImplicitDiff(
|
|
126
|
+
solver_kwargs={"ridge_identity": 1e-6},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
solver = sinkhorn.Sinkhorn(
|
|
130
|
+
lse_mode=True,
|
|
131
|
+
threshold=tol,
|
|
132
|
+
max_iterations=max_iter,
|
|
133
|
+
implicit_diff=implicit,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
out = solver(prob)
|
|
137
|
+
return out.matrix.astype(orig_dtype)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ----------------------------------------------------------------------- #
|
|
141
|
+
# Entropic projection via L-BFGS (requires optimistix + lineax)
|
|
142
|
+
# ----------------------------------------------------------------------- #
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _proj_transport_polytope_entropic_lbfgs(
|
|
146
|
+
C: jnp.ndarray, # (n, m)
|
|
147
|
+
mu: jnp.ndarray, # (n,)
|
|
148
|
+
nu: jnp.ndarray, # (m,)
|
|
149
|
+
epsilon: float | Array, # scalar
|
|
150
|
+
tol: float,
|
|
151
|
+
max_steps: int,
|
|
152
|
+
gauge_fix: bool = True,
|
|
153
|
+
implicit_diff: bool = True,
|
|
154
|
+
) -> jnp.ndarray:
|
|
155
|
+
"""Solve entropic OT via L-BFGS on the dual.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
C: Cost matrix of shape ``(n, m)``.
|
|
159
|
+
mu: Source marginal of shape ``(n,)``.
|
|
160
|
+
nu: Target marginal of shape ``(m,)``.
|
|
161
|
+
epsilon: Entropic regularization strength (scalar).
|
|
162
|
+
tol: Convergence tolerance.
|
|
163
|
+
max_steps: Maximum L-BFGS steps.
|
|
164
|
+
gauge_fix: If True, fix ``g[0] = 0`` to avoid singular
|
|
165
|
+
Hessians in implicit differentiation.
|
|
166
|
+
implicit_diff: If True, use implicit adjoint; otherwise
|
|
167
|
+
recursive checkpointing.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Transport plan of shape ``(n, m)``.
|
|
171
|
+
"""
|
|
172
|
+
if not HAS_OPTIMISTIX:
|
|
173
|
+
msg = (
|
|
174
|
+
"optimistix is required for L-BFGS transport "
|
|
175
|
+
"projection. Install it with: uv pip install optimistix"
|
|
176
|
+
)
|
|
177
|
+
raise ImportError(msg)
|
|
178
|
+
if not HAS_LINEAX:
|
|
179
|
+
msg = (
|
|
180
|
+
"lineax is required for L-BFGS transport "
|
|
181
|
+
"projection. Install it with: uv pip install lineax"
|
|
182
|
+
)
|
|
183
|
+
raise ImportError(msg)
|
|
184
|
+
|
|
185
|
+
orig_dtype = C.dtype
|
|
186
|
+
C, mu, nu, epsilon = _promote_transport_inputs(C, mu, nu, epsilon)
|
|
187
|
+
|
|
188
|
+
mu = jnp.clip(mu, 1e-12)
|
|
189
|
+
nu = jnp.clip(nu, 1e-12)
|
|
190
|
+
mu = mu / jnp.sum(mu)
|
|
191
|
+
nu = nu / jnp.sum(nu)
|
|
192
|
+
n, m = C.shape
|
|
193
|
+
|
|
194
|
+
if gauge_fix:
|
|
195
|
+
# Gauge fix: set g0 = 0, optimise f and g_rest to avoid
|
|
196
|
+
# singular system on implicit diff
|
|
197
|
+
y0 = (
|
|
198
|
+
jnp.zeros((n,), C.dtype),
|
|
199
|
+
jnp.zeros((m - 1,), C.dtype),
|
|
200
|
+
) # (f, g_rest)
|
|
201
|
+
else:
|
|
202
|
+
y0 = (
|
|
203
|
+
jnp.zeros((n,), C.dtype),
|
|
204
|
+
jnp.zeros((m,), C.dtype),
|
|
205
|
+
) # (f, g)
|
|
206
|
+
|
|
207
|
+
def neg_dual(
|
|
208
|
+
y: tuple[jnp.ndarray, jnp.ndarray],
|
|
209
|
+
args: tuple[
|
|
210
|
+
jnp.ndarray,
|
|
211
|
+
jnp.ndarray,
|
|
212
|
+
jnp.ndarray,
|
|
213
|
+
jnp.ndarray,
|
|
214
|
+
bool,
|
|
215
|
+
],
|
|
216
|
+
) -> jnp.ndarray:
|
|
217
|
+
"""Compute the negative entropic OT dual objective for L-BFGS minimization."""
|
|
218
|
+
C_, mu_, nu_, eps_, gauge_fix_ = args
|
|
219
|
+
if gauge_fix_:
|
|
220
|
+
f, g_rest = y
|
|
221
|
+
g = jnp.concatenate(
|
|
222
|
+
[jnp.zeros((1,), C_.dtype), g_rest],
|
|
223
|
+
axis=0,
|
|
224
|
+
) # (m,)
|
|
225
|
+
else:
|
|
226
|
+
f, g = y
|
|
227
|
+
Z = (f[:, None] + g[None, :] - C_) / eps_
|
|
228
|
+
return -(jnp.dot(mu_, f) + jnp.dot(nu_, g) - eps_ * jnp.sum(jnp.exp(Z)))
|
|
229
|
+
|
|
230
|
+
solver = optx.LBFGS(rtol=tol, atol=tol)
|
|
231
|
+
if implicit_diff:
|
|
232
|
+
adj = optx.ImplicitAdjoint(
|
|
233
|
+
linear_solver=lx.AutoLinearSolver(well_posed=False),
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
adj = optx.RecursiveCheckpointAdjoint()
|
|
237
|
+
sol = optx.minimise(
|
|
238
|
+
neg_dual,
|
|
239
|
+
solver=solver,
|
|
240
|
+
y0=y0,
|
|
241
|
+
args=(C, mu, nu, epsilon, gauge_fix),
|
|
242
|
+
max_steps=max_steps,
|
|
243
|
+
adjoint=adj,
|
|
244
|
+
throw=True,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if gauge_fix:
|
|
248
|
+
f, g_rest = sol.value
|
|
249
|
+
g = jnp.concatenate(
|
|
250
|
+
[jnp.zeros((1,), C.dtype), g_rest],
|
|
251
|
+
axis=0,
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
f, g = sol.value
|
|
255
|
+
Gamma = jnp.exp((f[:, None] + g[None, :] - C) / epsilon)
|
|
256
|
+
return Gamma.astype(orig_dtype)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ----------------------------------------------------------------------- #
|
|
260
|
+
# P-norm projection via L-BFGS (requires optimistix + lineax)
|
|
261
|
+
# ----------------------------------------------------------------------- #
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _proj_transport_polytope_pnorm_lbfgs(
|
|
265
|
+
C: jnp.ndarray, # (n, m)
|
|
266
|
+
mu: jnp.ndarray, # (n,)
|
|
267
|
+
nu: jnp.ndarray, # (m,)
|
|
268
|
+
lam: float | Array, # scalar
|
|
269
|
+
tol: float,
|
|
270
|
+
max_steps: int,
|
|
271
|
+
gauge_fix: bool = True,
|
|
272
|
+
p: float = 6 / 5, # 1 < p <= 2
|
|
273
|
+
implicit_diff: bool = True,
|
|
274
|
+
) -> jnp.ndarray:
|
|
275
|
+
"""Solve p-norm regularized OT via L-BFGS on the dual.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
C: Cost matrix of shape ``(n, m)``.
|
|
279
|
+
mu: Source marginal of shape ``(n,)``.
|
|
280
|
+
nu: Target marginal of shape ``(m,)``.
|
|
281
|
+
lam: Regularization strength (scalar).
|
|
282
|
+
tol: Convergence tolerance.
|
|
283
|
+
max_steps: Maximum L-BFGS steps.
|
|
284
|
+
gauge_fix: If True, fix ``g[0] = 0`` to avoid singular
|
|
285
|
+
Hessians in implicit differentiation.
|
|
286
|
+
p: Exponent for p-norm regularizer (1 < p <= 2).
|
|
287
|
+
implicit_diff: If True, use implicit adjoint; otherwise
|
|
288
|
+
recursive checkpointing.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Transport plan of shape ``(n, m)``.
|
|
292
|
+
"""
|
|
293
|
+
if not HAS_OPTIMISTIX:
|
|
294
|
+
msg = (
|
|
295
|
+
"optimistix is required for L-BFGS transport "
|
|
296
|
+
"projection. Install it with: uv pip install optimistix"
|
|
297
|
+
)
|
|
298
|
+
raise ImportError(msg)
|
|
299
|
+
if not HAS_LINEAX:
|
|
300
|
+
msg = (
|
|
301
|
+
"lineax is required for L-BFGS transport "
|
|
302
|
+
"projection. Install it with: uv pip install lineax"
|
|
303
|
+
)
|
|
304
|
+
raise ImportError(msg)
|
|
305
|
+
|
|
306
|
+
orig_dtype = C.dtype
|
|
307
|
+
C, mu, nu, lam = _promote_transport_inputs(C, mu, nu, lam)
|
|
308
|
+
|
|
309
|
+
mu = jnp.clip(mu, 1e-12)
|
|
310
|
+
nu = jnp.clip(nu, 1e-12)
|
|
311
|
+
mu = mu / jnp.sum(mu)
|
|
312
|
+
nu = nu / jnp.sum(nu)
|
|
313
|
+
n, m = C.shape
|
|
314
|
+
q = p / (p - 1.0) # conjugate exponent
|
|
315
|
+
lam_pow = lam ** (-(q - 1.0)) # lam^{-(q-1)}
|
|
316
|
+
|
|
317
|
+
if gauge_fix:
|
|
318
|
+
# Gauge fix: set g0 = 0, optimise f and g_rest to avoid
|
|
319
|
+
# singular system on implicit diff
|
|
320
|
+
y0 = (
|
|
321
|
+
jnp.zeros((n,), C.dtype),
|
|
322
|
+
jnp.zeros((m - 1,), C.dtype),
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
y0 = (
|
|
326
|
+
jnp.zeros((n,), C.dtype),
|
|
327
|
+
jnp.zeros((m,), C.dtype),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def neg_dual(
|
|
331
|
+
y: tuple[jnp.ndarray, jnp.ndarray],
|
|
332
|
+
args: tuple[
|
|
333
|
+
jnp.ndarray,
|
|
334
|
+
jnp.ndarray,
|
|
335
|
+
jnp.ndarray,
|
|
336
|
+
jnp.ndarray,
|
|
337
|
+
bool,
|
|
338
|
+
],
|
|
339
|
+
) -> jnp.ndarray:
|
|
340
|
+
"""Compute the negative p-norm OT dual objective for L-BFGS minimization."""
|
|
341
|
+
C_, mu_, nu_, lam_pow_, gauge_fix_ = args
|
|
342
|
+
if gauge_fix_:
|
|
343
|
+
f, g_rest = y
|
|
344
|
+
g = jnp.concatenate(
|
|
345
|
+
[jnp.zeros((1,), C_.dtype), g_rest],
|
|
346
|
+
axis=0,
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
f, g = y
|
|
350
|
+
S = f[:, None] + g[None, :] - C_
|
|
351
|
+
P = jnp.maximum(S, 0.0)
|
|
352
|
+
dual = jnp.dot(mu_, f) + jnp.dot(nu_, g) - (lam_pow_ / q) * jnp.sum(P**q)
|
|
353
|
+
return -dual
|
|
354
|
+
|
|
355
|
+
solver = optx.LBFGS(rtol=tol, atol=tol)
|
|
356
|
+
if implicit_diff:
|
|
357
|
+
adj = optx.ImplicitAdjoint(
|
|
358
|
+
linear_solver=lx.AutoLinearSolver(well_posed=False),
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
adj = optx.RecursiveCheckpointAdjoint()
|
|
362
|
+
sol = optx.minimise(
|
|
363
|
+
neg_dual,
|
|
364
|
+
solver=solver,
|
|
365
|
+
y0=y0,
|
|
366
|
+
args=(C, mu, nu, lam_pow, gauge_fix),
|
|
367
|
+
max_steps=max_steps,
|
|
368
|
+
adjoint=adj,
|
|
369
|
+
throw=True,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if gauge_fix:
|
|
373
|
+
f, g_rest = sol.value
|
|
374
|
+
g = jnp.concatenate(
|
|
375
|
+
[jnp.zeros((1,), C.dtype), g_rest],
|
|
376
|
+
axis=0,
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
f, g = sol.value
|
|
380
|
+
S = f[:, None] + g[None, :] - C
|
|
381
|
+
# = lambda^{-(q-1)} [S]_+^{q-1}
|
|
382
|
+
Gamma = lam_pow * jnp.maximum(S, 0.0) ** (q - 1.0)
|
|
383
|
+
return Gamma.astype(orig_dtype)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ----------------------------------------------------------------------- #
|
|
387
|
+
# Public dispatch function
|
|
388
|
+
# ----------------------------------------------------------------------- #
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def proj_transport_polytope(
|
|
392
|
+
cost: Array, # (..., n, m)
|
|
393
|
+
mu: Array, # ([n],)
|
|
394
|
+
nu: Array, # ([m],)
|
|
395
|
+
softness: float | Array = 0.1,
|
|
396
|
+
mode: Literal["smooth", "c0", "c1", "c2"] = "smooth",
|
|
397
|
+
use_entropic_ot_sinkhorn_on_entropic: bool = True,
|
|
398
|
+
sinkhorn_tol: float = 1e-5,
|
|
399
|
+
sinkhorn_max_iter: int = 10000,
|
|
400
|
+
lbfgs_tol: float = 1e-5,
|
|
401
|
+
lbfgs_max_iter: int = 10000,
|
|
402
|
+
implicit_diff: bool = True,
|
|
403
|
+
) -> Array: # (..., [n], m)
|
|
404
|
+
"""Project a cost matrix onto the transport polytope.
|
|
405
|
+
|
|
406
|
+
Solves the regularized optimal transport problem::
|
|
407
|
+
|
|
408
|
+
min_G <C, G> + softness * R(G)
|
|
409
|
+
s.t. G 1_m = mu, G^T 1_n = nu, G >= 0
|
|
410
|
+
|
|
411
|
+
where ``R(G)`` is the regularizer determined by ``mode``.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
cost: Input cost array of shape ``(..., n, m)``.
|
|
415
|
+
mu: Source marginal distribution of shape ``([n],)``.
|
|
416
|
+
nu: Target marginal distribution of shape ``([m],)``.
|
|
417
|
+
softness: Controls the strength of the regularizer.
|
|
418
|
+
Must be positive.
|
|
419
|
+
mode: Controls the type of regularizer:
|
|
420
|
+
|
|
421
|
+
- ``"smooth"``: C-infinity smooth (entropic/softmax
|
|
422
|
+
regularizer). Solved via Sinkhorn or L-BFGS.
|
|
423
|
+
- ``"c0"``: C0 continuous (Euclidean/L2 regularizer).
|
|
424
|
+
Solved via L-BFGS.
|
|
425
|
+
- ``"c1"``: C1 differentiable (p=3/2 p-norm). Solved
|
|
426
|
+
via L-BFGS.
|
|
427
|
+
- ``"c2"``: C2 twice differentiable (p=4/3 p-norm).
|
|
428
|
+
Solved via L-BFGS.
|
|
429
|
+
use_entropic_ot_sinkhorn_on_entropic: If True (default),
|
|
430
|
+
use Sinkhorn for ``"smooth"`` mode. If False, use
|
|
431
|
+
L-BFGS on the dual.
|
|
432
|
+
sinkhorn_tol: Convergence tolerance for Sinkhorn.
|
|
433
|
+
sinkhorn_max_iter: Maximum Sinkhorn iterations.
|
|
434
|
+
lbfgs_tol: Convergence tolerance for L-BFGS.
|
|
435
|
+
lbfgs_max_iter: Maximum L-BFGS iterations.
|
|
436
|
+
implicit_diff: If True (default), use implicit
|
|
437
|
+
differentiation for L-BFGS backward pass. More
|
|
438
|
+
numerically stable gradients, especially at low
|
|
439
|
+
softness.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Positive array of shape ``(..., [n], m)`` representing
|
|
443
|
+
the transport plan between ``mu`` and ``nu``. Sums to 1
|
|
444
|
+
over the second-to-last dimension and approximately sums
|
|
445
|
+
to 1 over the last dimension.
|
|
446
|
+
|
|
447
|
+
Note:
|
|
448
|
+
Internal solvers upcast to float64 when possible for
|
|
449
|
+
numerical stability. This requires
|
|
450
|
+
``jax.config.update("jax_enable_x64", True)`` (or the
|
|
451
|
+
``JAX_ENABLE_X64=1`` env var). Without it the upcast is
|
|
452
|
+
silently ignored and the solver may produce non-finite
|
|
453
|
+
gradients at larger problem sizes (typically n >= 2048).
|
|
454
|
+
"""
|
|
455
|
+
validate_softness(softness)
|
|
456
|
+
*batch_sizes, n, m = cost.shape
|
|
457
|
+
C = cost.reshape(-1, n, m) # (B, n, m)
|
|
458
|
+
|
|
459
|
+
if mode == "smooth":
|
|
460
|
+
use_entropic_ot_sinkhorn = use_entropic_ot_sinkhorn_on_entropic
|
|
461
|
+
|
|
462
|
+
if use_entropic_ot_sinkhorn:
|
|
463
|
+
proj_fn = lambda c: _proj_transport_polytope_entropic_sinkhorn(
|
|
464
|
+
c,
|
|
465
|
+
mu=mu,
|
|
466
|
+
nu=nu,
|
|
467
|
+
max_iter=sinkhorn_max_iter,
|
|
468
|
+
tol=sinkhorn_tol,
|
|
469
|
+
epsilon=softness,
|
|
470
|
+
)
|
|
471
|
+
else:
|
|
472
|
+
proj_fn = lambda c: _proj_transport_polytope_entropic_lbfgs(
|
|
473
|
+
c,
|
|
474
|
+
mu=mu,
|
|
475
|
+
nu=nu,
|
|
476
|
+
epsilon=softness,
|
|
477
|
+
tol=lbfgs_tol,
|
|
478
|
+
max_steps=lbfgs_max_iter,
|
|
479
|
+
implicit_diff=implicit_diff,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
else:
|
|
483
|
+
if mode == "c0":
|
|
484
|
+
# Curvature of (1/2)||y||^2 at transport polytope
|
|
485
|
+
# center: R''=1
|
|
486
|
+
p = 2
|
|
487
|
+
elif mode == "c1":
|
|
488
|
+
p = 3 / 2
|
|
489
|
+
elif mode == "c2":
|
|
490
|
+
p = 4 / 3
|
|
491
|
+
else:
|
|
492
|
+
msg = f"Invalid mode: {mode}"
|
|
493
|
+
raise ValueError(msg)
|
|
494
|
+
proj_fn = lambda c: _proj_transport_polytope_pnorm_lbfgs(
|
|
495
|
+
c,
|
|
496
|
+
mu=mu,
|
|
497
|
+
nu=nu,
|
|
498
|
+
lam=softness,
|
|
499
|
+
tol=lbfgs_tol,
|
|
500
|
+
max_steps=lbfgs_max_iter,
|
|
501
|
+
p=p,
|
|
502
|
+
implicit_diff=implicit_diff,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
Gamma = jax.vmap(proj_fn, in_axes=(0,))(C) # (B, n, m)
|
|
506
|
+
|
|
507
|
+
y = (Gamma * n).reshape(*batch_sizes, n, m) # (..., [n], m)
|
|
508
|
+
return y
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Soft bitonic sorting network.
|
|
2
|
+
|
|
3
|
+
Implements differentiable sorting and argsort using a bitonic sorting
|
|
4
|
+
network with soft compare-and-swap operations. The network has
|
|
5
|
+
O(n log^2 n) comparisons and produces smooth approximations to the
|
|
6
|
+
sorted output and permutation matrix.
|
|
7
|
+
|
|
8
|
+
Input is padded to the nearest power of 2 for the bitonic network,
|
|
9
|
+
then truncated back to the original length.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import jax
|
|
13
|
+
import jax.numpy as jnp
|
|
14
|
+
from jax import Array
|
|
15
|
+
|
|
16
|
+
from diffbio.core.soft_ops.elementwise import SigmoidalMode
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _soft_compare_and_swap(
|
|
20
|
+
a: Array,
|
|
21
|
+
b: Array,
|
|
22
|
+
softness: float | Array,
|
|
23
|
+
mode: SigmoidalMode,
|
|
24
|
+
) -> tuple[Array, Array, Array]:
|
|
25
|
+
"""Soft compare-and-swap via sigmoidal mixing.
|
|
26
|
+
|
|
27
|
+
Returns ``(soft_min, soft_max, sigma)`` where ``sigma`` is the
|
|
28
|
+
probability that ``a < b``.
|
|
29
|
+
"""
|
|
30
|
+
from diffbio.core.soft_ops.elementwise import sigmoidal
|
|
31
|
+
|
|
32
|
+
sigma = sigmoidal(a - b, softness=softness, mode=mode)
|
|
33
|
+
soft_min = sigma * b + (1.0 - sigma) * a
|
|
34
|
+
soft_max = sigma * a + (1.0 - sigma) * b
|
|
35
|
+
return soft_min, soft_max, sigma
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _bitonic_sort_ascending(
|
|
39
|
+
x: Array,
|
|
40
|
+
softness: float | Array,
|
|
41
|
+
mode: SigmoidalMode,
|
|
42
|
+
) -> Array:
|
|
43
|
+
"""1-D ascending bitonic sort. ``x`` must have power-of-2 length."""
|
|
44
|
+
n = x.shape[0]
|
|
45
|
+
num_phases = (n.bit_length() - 1) if n > 1 else 0
|
|
46
|
+
|
|
47
|
+
for phase in range(num_phases):
|
|
48
|
+
for sub_step in range(phase + 1):
|
|
49
|
+
d = 1 << (phase - sub_step)
|
|
50
|
+
indices = jnp.arange(n)
|
|
51
|
+
partner = indices ^ d
|
|
52
|
+
block_size = 1 << (phase + 1)
|
|
53
|
+
ascending_block = (indices & block_size) == 0
|
|
54
|
+
|
|
55
|
+
soft_min, soft_max, _ = _soft_compare_and_swap(
|
|
56
|
+
x,
|
|
57
|
+
x[partner],
|
|
58
|
+
softness,
|
|
59
|
+
mode,
|
|
60
|
+
)
|
|
61
|
+
x = jnp.where(
|
|
62
|
+
ascending_block,
|
|
63
|
+
jnp.where(indices < partner, soft_min, soft_max),
|
|
64
|
+
jnp.where(indices < partner, soft_max, soft_min),
|
|
65
|
+
)
|
|
66
|
+
return x
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def sort_via_sorting_network(
|
|
70
|
+
x: Array,
|
|
71
|
+
softness: float | Array,
|
|
72
|
+
mode: SigmoidalMode,
|
|
73
|
+
descending: bool,
|
|
74
|
+
standardized: bool = False,
|
|
75
|
+
) -> Array:
|
|
76
|
+
"""Sort along the last axis using a soft bitonic sorting network.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
x: Input array of shape ``(..., n)``.
|
|
80
|
+
softness: Controls sharpness of compare-and-swap.
|
|
81
|
+
mode: Smoothness mode for sigmoidal.
|
|
82
|
+
descending: If True, sort in descending order.
|
|
83
|
+
standardized: If True, input is already in (0, 1) from sigmoid.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Sorted array of shape ``(..., n)``.
|
|
87
|
+
"""
|
|
88
|
+
*batch_shape, n = x.shape
|
|
89
|
+
|
|
90
|
+
n_padded = 1 << (n - 1).bit_length() if n > 1 else 2
|
|
91
|
+
if n_padded > n:
|
|
92
|
+
pad_val = 1.0 if standardized else jnp.max(x) + 1.0
|
|
93
|
+
pad_width = [(0, 0)] * len(batch_shape) + [(0, n_padded - n)]
|
|
94
|
+
x = jnp.pad(x, pad_width, constant_values=pad_val)
|
|
95
|
+
|
|
96
|
+
if batch_shape:
|
|
97
|
+
x_flat = x.reshape(-1, n_padded)
|
|
98
|
+
sorted_flat = jax.vmap(
|
|
99
|
+
lambda row: _bitonic_sort_ascending(row, softness, mode),
|
|
100
|
+
)(x_flat)
|
|
101
|
+
sorted_x = sorted_flat.reshape(*batch_shape, n_padded)
|
|
102
|
+
else:
|
|
103
|
+
sorted_x = _bitonic_sort_ascending(x, softness, mode)
|
|
104
|
+
|
|
105
|
+
if n_padded > n:
|
|
106
|
+
sorted_x = sorted_x[..., :n]
|
|
107
|
+
if descending:
|
|
108
|
+
sorted_x = jnp.flip(sorted_x, axis=-1)
|
|
109
|
+
return sorted_x
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _bitonic_argsort_ascending(
|
|
113
|
+
x: Array,
|
|
114
|
+
softness: float | Array,
|
|
115
|
+
mode: SigmoidalMode,
|
|
116
|
+
) -> tuple[Array, Array]:
|
|
117
|
+
"""1-D ascending bitonic sort with permutation tracking.
|
|
118
|
+
|
|
119
|
+
Returns ``(sorted_x, P)`` where ``P`` is an ``(n, n)`` soft
|
|
120
|
+
permutation matrix: ``P[sorted_pos, original_elem]`` is the
|
|
121
|
+
probability that ``original_elem`` ends up at ``sorted_pos``.
|
|
122
|
+
"""
|
|
123
|
+
n = x.shape[0]
|
|
124
|
+
perm = jnp.eye(n)
|
|
125
|
+
num_phases = (n.bit_length() - 1) if n > 1 else 0
|
|
126
|
+
|
|
127
|
+
for phase in range(num_phases):
|
|
128
|
+
for sub_step in range(phase + 1):
|
|
129
|
+
d = 1 << (phase - sub_step)
|
|
130
|
+
indices = jnp.arange(n)
|
|
131
|
+
partner = indices ^ d
|
|
132
|
+
block_size = 1 << (phase + 1)
|
|
133
|
+
ascending_block = (indices & block_size) == 0
|
|
134
|
+
|
|
135
|
+
soft_min, soft_max, sigma = _soft_compare_and_swap(
|
|
136
|
+
x,
|
|
137
|
+
x[partner],
|
|
138
|
+
softness,
|
|
139
|
+
mode,
|
|
140
|
+
)
|
|
141
|
+
x = jnp.where(
|
|
142
|
+
ascending_block,
|
|
143
|
+
jnp.where(indices < partner, soft_min, soft_max),
|
|
144
|
+
jnp.where(indices < partner, soft_max, soft_min),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
gets_min = (ascending_block & (indices < partner)) | (
|
|
148
|
+
~ascending_block & (indices >= partner)
|
|
149
|
+
)
|
|
150
|
+
mix = jnp.where(gets_min, sigma, 1.0 - sigma)
|
|
151
|
+
|
|
152
|
+
perm_partner = perm[partner]
|
|
153
|
+
perm = (1.0 - mix[:, None]) * perm + mix[:, None] * perm_partner
|
|
154
|
+
|
|
155
|
+
return x, perm
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def argsort_via_sorting_network(
|
|
159
|
+
x: Array,
|
|
160
|
+
softness: float | Array,
|
|
161
|
+
mode: SigmoidalMode,
|
|
162
|
+
descending: bool,
|
|
163
|
+
standardized: bool = False,
|
|
164
|
+
) -> Array:
|
|
165
|
+
"""Argsort via soft bitonic sorting network.
|
|
166
|
+
|
|
167
|
+
Returns a soft permutation matrix ``P`` of shape ``(..., n, n)``
|
|
168
|
+
where ``P[..., sorted_pos, original_elem]`` is the probability
|
|
169
|
+
that ``original_elem`` ends up at ``sorted_pos``.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
x: Input array of shape ``(..., n)``.
|
|
173
|
+
softness: Controls sharpness of compare-and-swap.
|
|
174
|
+
mode: Smoothness mode for sigmoidal.
|
|
175
|
+
descending: If True, reverse the sort order.
|
|
176
|
+
standardized: If True, input is already in (0, 1).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Soft permutation matrix of shape ``(..., n, n)``.
|
|
180
|
+
"""
|
|
181
|
+
*batch_shape, n = x.shape
|
|
182
|
+
|
|
183
|
+
n_padded = 1 << (n - 1).bit_length() if n > 1 else 2
|
|
184
|
+
if n_padded > n:
|
|
185
|
+
pad_val = 1.0 if standardized else jnp.max(x) + 1.0
|
|
186
|
+
pad_width = [(0, 0)] * len(batch_shape) + [(0, n_padded - n)]
|
|
187
|
+
x = jnp.pad(x, pad_width, constant_values=pad_val)
|
|
188
|
+
|
|
189
|
+
if batch_shape:
|
|
190
|
+
x_flat = x.reshape(-1, n_padded)
|
|
191
|
+
_, perm_flat = jax.vmap(
|
|
192
|
+
lambda row: _bitonic_argsort_ascending(row, softness, mode),
|
|
193
|
+
)(x_flat)
|
|
194
|
+
perm = perm_flat.reshape(*batch_shape, n_padded, n_padded)
|
|
195
|
+
else:
|
|
196
|
+
_, perm = _bitonic_argsort_ascending(x, softness, mode)
|
|
197
|
+
|
|
198
|
+
if n_padded > n:
|
|
199
|
+
perm = perm[..., :n, :n]
|
|
200
|
+
perm = perm / jnp.clip(jnp.sum(perm, axis=-1, keepdims=True), min=1e-10)
|
|
201
|
+
|
|
202
|
+
if descending:
|
|
203
|
+
perm = jnp.flip(perm, axis=-2)
|
|
204
|
+
return perm
|