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,1864 @@
|
|
|
1
|
+
"""Differentiable projections onto the permutahedron.
|
|
2
|
+
|
|
3
|
+
Provides multiple projection algorithms with varying smoothness:
|
|
4
|
+
- Entropic (log-KL) projection via isotonic regression (``smooth`` mode)
|
|
5
|
+
- Euclidean / L2 projection via PAV (``c0`` mode)
|
|
6
|
+
- p-norm p=3/2 projection via PAV with closed-form block solvers (``c1``)
|
|
7
|
+
- p-norm p=4/3 projection via PAV with closed-form block solvers (``c2``)
|
|
8
|
+
- C-infinity smooth ESP+LBFGS method (``smooth_sort`` mode)
|
|
9
|
+
|
|
10
|
+
All implementations preserve exact mathematical behavior and support
|
|
11
|
+
JAX autodiff via custom VJP rules.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
import jax
|
|
19
|
+
import jax.numpy as jnp
|
|
20
|
+
from jax import Array, lax
|
|
21
|
+
from jax.ops import segment_sum
|
|
22
|
+
|
|
23
|
+
from diffbio.core.soft_ops._utils import validate_softness
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import optimistix as optx
|
|
27
|
+
|
|
28
|
+
HAS_OPTIMISTIX = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
HAS_OPTIMISTIX = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _high_precision_dtype() -> jnp.dtype:
|
|
34
|
+
"""Return float64 when x64 is enabled, otherwise float32."""
|
|
35
|
+
return jnp.result_type(float)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _inv_permutation(p: jax.Array) -> jax.Array:
|
|
39
|
+
"""Compute the inverse of a permutation array."""
|
|
40
|
+
inv = jnp.empty_like(p)
|
|
41
|
+
return inv.at[p].set(jnp.arange(p.shape[0], dtype=p.dtype))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# -- smooth: entropic LP-LBFGS ----------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _diff_T(beta: jax.Array) -> jax.Array:
|
|
48
|
+
"""Compute D^T beta where D is the (n-1)x n difference matrix.
|
|
49
|
+
|
|
50
|
+
D is defined by (Dy)_i = y_i - y_{i+1}.
|
|
51
|
+
D^T beta = [beta0, beta1-beta0, ..., beta_{n-2}-beta_{n-3},
|
|
52
|
+
-beta_{n-2}].
|
|
53
|
+
"""
|
|
54
|
+
left = jnp.concatenate(
|
|
55
|
+
[jnp.zeros((1,), dtype=beta.dtype), beta],
|
|
56
|
+
axis=0,
|
|
57
|
+
) # (n,)
|
|
58
|
+
right = jnp.concatenate(
|
|
59
|
+
[beta, jnp.zeros((1,), dtype=beta.dtype)],
|
|
60
|
+
axis=0,
|
|
61
|
+
) # (n,)
|
|
62
|
+
return right - left
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _alpha_nu_from_beta(
|
|
66
|
+
z_s: jax.Array,
|
|
67
|
+
beta: jax.Array,
|
|
68
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
69
|
+
"""Recover dual variables alpha and nu from beta.
|
|
70
|
+
|
|
71
|
+
Uses stationarity: z_s - A^T alpha + D^T beta - nu*1 = 0
|
|
72
|
+
with (A^T alpha)_n = 0.
|
|
73
|
+
"""
|
|
74
|
+
dtb = _diff_T(beta) # (n,)
|
|
75
|
+
nu = z_s[-1] + dtb[-1] # scalar, enforces last component = 0
|
|
76
|
+
u = z_s + dtb - nu # (n,), u[-1] == 0
|
|
77
|
+
alpha = u[:-1] - u[1:] # (n-1,), since u_i - u_{i+1} = alpha_i
|
|
78
|
+
return alpha, nu
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _reconstruct_y_from_slacks_and_gaps(
|
|
82
|
+
b: jax.Array, # (n-1,) prefix bounds
|
|
83
|
+
b_n: jax.Array, # scalar sum bound
|
|
84
|
+
s: jax.Array, # (n-1,) prefix slacks, s>0
|
|
85
|
+
d: jax.Array, # (n-1,) gaps, d>0
|
|
86
|
+
) -> jax.Array:
|
|
87
|
+
"""Reconstruct primal y from slacks and gaps.
|
|
88
|
+
|
|
89
|
+
We want Ay = b - s and Dy = d and 1^T y = b_n.
|
|
90
|
+
Uses d to parameterize y = y_n + tail_sums(d), then picks
|
|
91
|
+
y_n to best match Ay=b-s (including the sum equation k=n).
|
|
92
|
+
"""
|
|
93
|
+
n = d.shape[0] + 1
|
|
94
|
+
dtype = d.dtype
|
|
95
|
+
|
|
96
|
+
# tail sums t_i = sum_{j=i}^{n-1} d_j, with t_n = 0
|
|
97
|
+
tail = jnp.flip(
|
|
98
|
+
jnp.cumsum(jnp.flip(d, axis=0), axis=0),
|
|
99
|
+
axis=0,
|
|
100
|
+
) # (n-1,)
|
|
101
|
+
t = jnp.concatenate(
|
|
102
|
+
[tail, jnp.zeros((1,), dtype=dtype)],
|
|
103
|
+
axis=0,
|
|
104
|
+
) # (n,)
|
|
105
|
+
|
|
106
|
+
# prefix sums of t: T_k = sum_{i<=k} t_i, k=1..n-1
|
|
107
|
+
T = jnp.cumsum(t, axis=0)[:-1] # (n-1,)
|
|
108
|
+
|
|
109
|
+
r = b - s # target prefix sums of y, (n-1,)
|
|
110
|
+
rhs = r - T # (n-1,) rhs_k in k*y_n = rhs_k
|
|
111
|
+
|
|
112
|
+
ks = jnp.arange(1, n, dtype=dtype) # 1..n-1
|
|
113
|
+
rhs_n = b_n - jnp.sum(t) # n*y_n = rhs_n
|
|
114
|
+
|
|
115
|
+
num = jnp.sum(ks * rhs) + (n * rhs_n)
|
|
116
|
+
den = jnp.sum(ks * ks) + (n * n)
|
|
117
|
+
y_n = num / den
|
|
118
|
+
|
|
119
|
+
y = y_n + t
|
|
120
|
+
return y
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _smooth_majorization_bounds(
|
|
124
|
+
w: jax.Array,
|
|
125
|
+
tau: jax.Array,
|
|
126
|
+
checkpointed: bool = True,
|
|
127
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
128
|
+
"""C-infinity smooth majorization bounds via ESPs.
|
|
129
|
+
|
|
130
|
+
Computes b_k = tau * log e_k(exp(w/tau)) for k = 1 ... n-1,
|
|
131
|
+
where e_k is the k-th elementary symmetric polynomial.
|
|
132
|
+
|
|
133
|
+
This is the log-sum-exp over all size-k subsets:
|
|
134
|
+
b_k = tau * log sum_{|S|=k} exp(sum_{i in S} w_i / tau)
|
|
135
|
+
and serves as a C-infinity relaxation of the hard order-statistic
|
|
136
|
+
partial sums b_k = sum_{j<=k} w_{(j)}.
|
|
137
|
+
|
|
138
|
+
Uses the recurrence
|
|
139
|
+
E[k][j] = E[k][j-1] + exp(w_j/tau) * E[k-1][j-1]
|
|
140
|
+
in log-space (logaddexp) for numerical stability. O(n^2).
|
|
141
|
+
|
|
142
|
+
When ``checkpointed=True`` (default), uses optimal online gradient
|
|
143
|
+
checkpointing (Stumm & Walther 2010) to reduce memory from O(n^2)
|
|
144
|
+
to O(n*sqrt(n)) at the cost of ~2x forward compute during backward.
|
|
145
|
+
"""
|
|
146
|
+
n = w.shape[0]
|
|
147
|
+
x = w / tau # (n,)
|
|
148
|
+
|
|
149
|
+
# Use a large finite sentinel instead of -inf to avoid NaN grads
|
|
150
|
+
# from logaddexp(-inf, -inf) at impossible subset sizes (k > j+1).
|
|
151
|
+
_FLOOR = jnp.array(-1e30, dtype=w.dtype)
|
|
152
|
+
# log_E[k] = log(E[k][j]) after processing j elements; k = 0..n-1
|
|
153
|
+
log_E = jnp.full((n,), _FLOOR, dtype=w.dtype)
|
|
154
|
+
log_E = log_E.at[0].set(0.0) # e_0 = 1
|
|
155
|
+
|
|
156
|
+
def _scan_step(
|
|
157
|
+
log_E: jax.Array,
|
|
158
|
+
x_j: jax.Array,
|
|
159
|
+
) -> tuple[jax.Array, None]:
|
|
160
|
+
"""Update log-ESP state by incorporating element x_j."""
|
|
161
|
+
# log E[k][j] = logaddexp(
|
|
162
|
+
# log E[k][j-1], x_j + log E[k-1][j-1]
|
|
163
|
+
# )
|
|
164
|
+
log_E_prev = jnp.concatenate(
|
|
165
|
+
[jnp.array([_FLOOR], dtype=log_E.dtype), log_E[:-1]],
|
|
166
|
+
)
|
|
167
|
+
log_E = jnp.logaddexp(log_E, x_j + log_E_prev)
|
|
168
|
+
return log_E, None
|
|
169
|
+
|
|
170
|
+
if checkpointed:
|
|
171
|
+
|
|
172
|
+
@jax.checkpoint
|
|
173
|
+
def _checkpointed_scan_step(
|
|
174
|
+
log_E: jax.Array,
|
|
175
|
+
x_j: jax.Array,
|
|
176
|
+
) -> tuple[jax.Array, None]:
|
|
177
|
+
"""Gradient-checkpointed wrapper around _scan_step."""
|
|
178
|
+
return _scan_step(log_E, x_j)
|
|
179
|
+
|
|
180
|
+
log_E, _ = lax.scan(_checkpointed_scan_step, log_E, x)
|
|
181
|
+
else:
|
|
182
|
+
log_E, _ = lax.scan(_scan_step, log_E, x)
|
|
183
|
+
|
|
184
|
+
# b_k = tau * log(e_k) for k = 1..n-1
|
|
185
|
+
b = tau * log_E[1:] # (n-1,)
|
|
186
|
+
b_n = jnp.sum(w) # exact: tau * log(e_n) = sum w_i
|
|
187
|
+
return b, b_n
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _tridiag_L_matvec(r: jax.Array) -> jax.Array:
|
|
191
|
+
"""Compute L*r where L is the tridiagonal second-difference matrix.
|
|
192
|
+
|
|
193
|
+
L[i,i] = 2, L[i,i-1] = L[i,i+1] = -1.
|
|
194
|
+
Result: (L*r)_i = 2*r[i] - r[i-1] - r[i+1], with boundary handling.
|
|
195
|
+
"""
|
|
196
|
+
result = 2.0 * r
|
|
197
|
+
result = result.at[:-1].add(-r[1:])
|
|
198
|
+
result = result.at[1:].add(-r[:-1])
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _proj_permutahedron_entropic_lp_lbfgs(
|
|
203
|
+
z: jax.Array, # (n,)
|
|
204
|
+
w: jax.Array, # (n,)
|
|
205
|
+
softness: float = 1.0, # tau for majorization slacks
|
|
206
|
+
softness_mono: float | None = None, # tau_m for mono gaps
|
|
207
|
+
max_iter: int = 200,
|
|
208
|
+
tol: float = 1e-5,
|
|
209
|
+
history_length: int = 5,
|
|
210
|
+
l2_beta: float = 0.0, # optional stabilizer on beta
|
|
211
|
+
throw: bool = True,
|
|
212
|
+
) -> jax.Array:
|
|
213
|
+
"""Linear projection via entropic regularization on slacks and gaps.
|
|
214
|
+
|
|
215
|
+
Solves the eliminated dual over beta (unconstrained) via
|
|
216
|
+
optimistix.LBFGS.
|
|
217
|
+
|
|
218
|
+
Requires the ``optimistix`` package.
|
|
219
|
+
"""
|
|
220
|
+
if not HAS_OPTIMISTIX:
|
|
221
|
+
msg = (
|
|
222
|
+
"optimistix is required for "
|
|
223
|
+
"_proj_permutahedron_entropic_lp_lbfgs. "
|
|
224
|
+
"Install it with: pip install optimistix"
|
|
225
|
+
)
|
|
226
|
+
raise ImportError(msg)
|
|
227
|
+
z = jnp.asarray(z)
|
|
228
|
+
w = jnp.asarray(w)
|
|
229
|
+
if z.ndim != 1 or w.ndim != 1 or z.shape != w.shape:
|
|
230
|
+
msg = f"z,w must be 1D and same shape; got {z.shape}, {w.shape}"
|
|
231
|
+
raise ValueError(msg)
|
|
232
|
+
n = z.shape[0]
|
|
233
|
+
if n <= 1:
|
|
234
|
+
return w
|
|
235
|
+
|
|
236
|
+
# Upcast to highest available float for solver precision.
|
|
237
|
+
orig_dtype = z.dtype
|
|
238
|
+
_hp = _high_precision_dtype()
|
|
239
|
+
if z.dtype != _hp:
|
|
240
|
+
z = z.astype(_hp)
|
|
241
|
+
w = w.astype(_hp)
|
|
242
|
+
|
|
243
|
+
tau = jnp.asarray(softness, dtype=z.dtype)
|
|
244
|
+
tau_m = tau if softness_mono is None else jnp.asarray(softness_mono, dtype=z.dtype)
|
|
245
|
+
|
|
246
|
+
# choose chamber by sorting z
|
|
247
|
+
pz = jnp.argsort(-z, stable=True)
|
|
248
|
+
iz = _inv_permutation(pz)
|
|
249
|
+
z_s = z[pz]
|
|
250
|
+
|
|
251
|
+
# permutahedron bounds from sorted w
|
|
252
|
+
pw = jnp.argsort(-w, stable=True)
|
|
253
|
+
w_s = w[pw]
|
|
254
|
+
b_full = jnp.cumsum(w_s)
|
|
255
|
+
b = b_full[:-1] # (n-1,)
|
|
256
|
+
b_n = b_full[-1] # scalar
|
|
257
|
+
|
|
258
|
+
# dual after elimination: minimize
|
|
259
|
+
# g(beta) = <alpha(beta), b> + nu(beta)*b_n
|
|
260
|
+
# + tau * sum exp(-alpha/tau)
|
|
261
|
+
# + tau_m * sum exp(-beta/tau_m)
|
|
262
|
+
# where alpha,nu satisfy stationarity.
|
|
263
|
+
def dual_obj(
|
|
264
|
+
beta: jax.Array,
|
|
265
|
+
args: tuple,
|
|
266
|
+
) -> jax.Array:
|
|
267
|
+
"""Evaluate the eliminated dual objective over beta."""
|
|
268
|
+
z_s, b, b_n, tau, tau_m = args
|
|
269
|
+
alpha, nu = _alpha_nu_from_beta(z_s, beta)
|
|
270
|
+
|
|
271
|
+
val = jnp.dot(alpha, b) + nu * b_n
|
|
272
|
+
val = val + tau * jnp.sum(jnp.exp(-alpha / tau))
|
|
273
|
+
val = val + tau_m * jnp.sum(jnp.exp(-beta / tau_m))
|
|
274
|
+
if l2_beta:
|
|
275
|
+
val = val + 0.5 * jnp.asarray(
|
|
276
|
+
l2_beta,
|
|
277
|
+
z.dtype,
|
|
278
|
+
) * jnp.sum(beta * beta)
|
|
279
|
+
return val
|
|
280
|
+
|
|
281
|
+
beta0 = jnp.zeros((n - 1,), dtype=z.dtype)
|
|
282
|
+
solver = optx.LBFGS(
|
|
283
|
+
rtol=tol,
|
|
284
|
+
atol=tol,
|
|
285
|
+
history_length=history_length,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
sol = optx.minimise(
|
|
289
|
+
dual_obj,
|
|
290
|
+
solver,
|
|
291
|
+
beta0,
|
|
292
|
+
args=(z_s, b, b_n, tau, tau_m),
|
|
293
|
+
max_steps=max_iter,
|
|
294
|
+
throw=throw,
|
|
295
|
+
)
|
|
296
|
+
beta_star = sol.value
|
|
297
|
+
|
|
298
|
+
alpha_star, _ = _alpha_nu_from_beta(z_s, beta_star)
|
|
299
|
+
|
|
300
|
+
# primal variables from KKT
|
|
301
|
+
s = jnp.exp(-alpha_star / tau) # (n-1,)
|
|
302
|
+
d = jnp.exp(-beta_star / tau_m) # (n-1,)
|
|
303
|
+
|
|
304
|
+
y_s = _reconstruct_y_from_slacks_and_gaps(b, b_n, s, d)
|
|
305
|
+
result = y_s[iz]
|
|
306
|
+
if result.dtype != orig_dtype:
|
|
307
|
+
result = result.astype(orig_dtype)
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _hessian_beta_matvec(
|
|
312
|
+
v: jax.Array,
|
|
313
|
+
w_alpha: jax.Array,
|
|
314
|
+
w_beta: jax.Array,
|
|
315
|
+
) -> jax.Array:
|
|
316
|
+
"""Matvec with L*diag(w_a)*L*v + diag(w_b)*v. L is symmetric."""
|
|
317
|
+
Lv = _tridiag_L_matvec(v)
|
|
318
|
+
return _tridiag_L_matvec(w_alpha * Lv) + w_beta * v
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _make_proj_permutahedron_entropic_lp(
|
|
322
|
+
tol: float = 1e-5,
|
|
323
|
+
max_iter: int = 200,
|
|
324
|
+
softness: float = 1.0,
|
|
325
|
+
softness_mono: float | None = None,
|
|
326
|
+
bounds_softness: float | Array = 1.0,
|
|
327
|
+
checkpointed: bool = True,
|
|
328
|
+
) -> callable:
|
|
329
|
+
"""Return a ``(z, w) -> result`` function with C-inf smooth grads.
|
|
330
|
+
|
|
331
|
+
Uses smooth majorization bounds (elementary symmetric polynomials)
|
|
332
|
+
for C-infinity gradients w.r.t. ``w``. The ``custom_vjp`` wraps
|
|
333
|
+
only the LBFGS solver ``(z, b, b_n) -> y``, providing analytical
|
|
334
|
+
gradients for ``z`` and pass-through gradients for ``b`` / ``b_n``.
|
|
335
|
+
Gradients from ``b`` / ``b_n`` to ``w`` flow via standard JAX
|
|
336
|
+
autodiff through :func:`_smooth_majorization_bounds`.
|
|
337
|
+
|
|
338
|
+
Requires the ``optimistix`` package.
|
|
339
|
+
"""
|
|
340
|
+
if not HAS_OPTIMISTIX:
|
|
341
|
+
msg = (
|
|
342
|
+
"optimistix is required for "
|
|
343
|
+
"_make_proj_permutahedron_entropic_lp. "
|
|
344
|
+
"Install it with: pip install optimistix"
|
|
345
|
+
)
|
|
346
|
+
raise ImportError(msg)
|
|
347
|
+
|
|
348
|
+
# -- inner solver with custom_vjp on (z, b, b_n) --
|
|
349
|
+
|
|
350
|
+
@jax.custom_vjp
|
|
351
|
+
def _solver(
|
|
352
|
+
z: jax.Array,
|
|
353
|
+
b: jax.Array,
|
|
354
|
+
b_n: jax.Array,
|
|
355
|
+
) -> jax.Array:
|
|
356
|
+
"""Solve the entropic LP dual via LBFGS and recover primal y."""
|
|
357
|
+
orig_dtype = z.dtype
|
|
358
|
+
_hp = _high_precision_dtype()
|
|
359
|
+
if z.dtype != _hp:
|
|
360
|
+
z = z.astype(_hp)
|
|
361
|
+
b = b.astype(_hp)
|
|
362
|
+
b_n = b_n.astype(_hp)
|
|
363
|
+
|
|
364
|
+
tau = jnp.asarray(softness, dtype=z.dtype)
|
|
365
|
+
tau_m = tau if softness_mono is None else jnp.asarray(softness_mono, dtype=z.dtype)
|
|
366
|
+
|
|
367
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
368
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
369
|
+
z_s = z[perm_z]
|
|
370
|
+
|
|
371
|
+
def dual_obj(
|
|
372
|
+
beta: jax.Array,
|
|
373
|
+
args: tuple,
|
|
374
|
+
) -> jax.Array:
|
|
375
|
+
"""Evaluate the entropic dual objective for LBFGS."""
|
|
376
|
+
z_s_, b_, b_n_, tau_, tau_m_ = args
|
|
377
|
+
alpha, nu = _alpha_nu_from_beta(z_s_, beta)
|
|
378
|
+
val = jnp.dot(alpha, b_) + nu * b_n_
|
|
379
|
+
val = val + tau_ * jnp.sum(
|
|
380
|
+
jnp.exp(-alpha / tau_),
|
|
381
|
+
)
|
|
382
|
+
val = val + tau_m_ * jnp.sum(
|
|
383
|
+
jnp.exp(-beta / tau_m_),
|
|
384
|
+
)
|
|
385
|
+
return val
|
|
386
|
+
|
|
387
|
+
n = z.shape[0]
|
|
388
|
+
beta0 = jnp.zeros((n - 1,), dtype=z.dtype)
|
|
389
|
+
solver_lbfgs = optx.LBFGS(
|
|
390
|
+
rtol=tol,
|
|
391
|
+
atol=tol,
|
|
392
|
+
history_length=5,
|
|
393
|
+
)
|
|
394
|
+
sol = optx.minimise(
|
|
395
|
+
dual_obj,
|
|
396
|
+
solver_lbfgs,
|
|
397
|
+
beta0,
|
|
398
|
+
args=(z_s, b, b_n, tau, tau_m),
|
|
399
|
+
max_steps=max_iter,
|
|
400
|
+
throw=False,
|
|
401
|
+
)
|
|
402
|
+
beta_star = sol.value
|
|
403
|
+
alpha_star, _ = _alpha_nu_from_beta(z_s, beta_star)
|
|
404
|
+
|
|
405
|
+
s = jnp.exp(-alpha_star / tau)
|
|
406
|
+
d = jnp.exp(-beta_star / tau_m)
|
|
407
|
+
|
|
408
|
+
y_s = _reconstruct_y_from_slacks_and_gaps(
|
|
409
|
+
b,
|
|
410
|
+
b_n,
|
|
411
|
+
s,
|
|
412
|
+
d,
|
|
413
|
+
)
|
|
414
|
+
result = y_s[inv_perm_z]
|
|
415
|
+
if result.dtype != orig_dtype:
|
|
416
|
+
result = result.astype(orig_dtype)
|
|
417
|
+
return result
|
|
418
|
+
|
|
419
|
+
def _solver_fwd(
|
|
420
|
+
z: jax.Array,
|
|
421
|
+
b: jax.Array,
|
|
422
|
+
b_n: jax.Array,
|
|
423
|
+
) -> tuple[jax.Array, tuple]:
|
|
424
|
+
"""Forward pass for the entropic LP solver custom VJP."""
|
|
425
|
+
z = jnp.asarray(z)
|
|
426
|
+
b = jnp.asarray(b)
|
|
427
|
+
b_n = jnp.asarray(b_n)
|
|
428
|
+
n = z.shape[0]
|
|
429
|
+
|
|
430
|
+
orig_dtype = z.dtype
|
|
431
|
+
_hp = _high_precision_dtype()
|
|
432
|
+
if z.dtype != _hp:
|
|
433
|
+
z = z.astype(_hp)
|
|
434
|
+
b = b.astype(_hp)
|
|
435
|
+
b_n = b_n.astype(_hp)
|
|
436
|
+
|
|
437
|
+
tau = jnp.asarray(softness, dtype=z.dtype)
|
|
438
|
+
tau_m = tau if softness_mono is None else jnp.asarray(softness_mono, dtype=z.dtype)
|
|
439
|
+
|
|
440
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
441
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
442
|
+
z_s = z[perm_z]
|
|
443
|
+
|
|
444
|
+
if n <= 1:
|
|
445
|
+
result = jnp.full_like(z, b_n)
|
|
446
|
+
if orig_dtype != _hp:
|
|
447
|
+
result = result.astype(orig_dtype)
|
|
448
|
+
s = jnp.empty((0,), dtype=z.dtype)
|
|
449
|
+
d = jnp.empty((0,), dtype=z.dtype)
|
|
450
|
+
return result, (
|
|
451
|
+
perm_z,
|
|
452
|
+
inv_perm_z,
|
|
453
|
+
s,
|
|
454
|
+
d,
|
|
455
|
+
tau,
|
|
456
|
+
tau_m,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def dual_obj(
|
|
460
|
+
beta: jax.Array,
|
|
461
|
+
args: tuple,
|
|
462
|
+
) -> jax.Array:
|
|
463
|
+
"""Evaluate the entropic dual objective for forward LBFGS."""
|
|
464
|
+
z_s_, b_, b_n_, tau_, tau_m_ = args
|
|
465
|
+
alpha, nu = _alpha_nu_from_beta(z_s_, beta)
|
|
466
|
+
val = jnp.dot(alpha, b_) + nu * b_n_
|
|
467
|
+
val = val + tau_ * jnp.sum(
|
|
468
|
+
jnp.exp(-alpha / tau_),
|
|
469
|
+
)
|
|
470
|
+
val = val + tau_m_ * jnp.sum(
|
|
471
|
+
jnp.exp(-beta / tau_m_),
|
|
472
|
+
)
|
|
473
|
+
return val
|
|
474
|
+
|
|
475
|
+
beta0 = jnp.zeros((n - 1,), dtype=z.dtype)
|
|
476
|
+
solver_lbfgs = optx.LBFGS(
|
|
477
|
+
rtol=tol,
|
|
478
|
+
atol=tol,
|
|
479
|
+
history_length=5,
|
|
480
|
+
)
|
|
481
|
+
sol = optx.minimise(
|
|
482
|
+
dual_obj,
|
|
483
|
+
solver_lbfgs,
|
|
484
|
+
beta0,
|
|
485
|
+
args=(z_s, b, b_n, tau, tau_m),
|
|
486
|
+
max_steps=max_iter,
|
|
487
|
+
throw=False,
|
|
488
|
+
)
|
|
489
|
+
beta_star = sol.value
|
|
490
|
+
alpha_star, _ = _alpha_nu_from_beta(z_s, beta_star)
|
|
491
|
+
|
|
492
|
+
s = jnp.exp(-alpha_star / tau)
|
|
493
|
+
d = jnp.exp(-beta_star / tau_m)
|
|
494
|
+
|
|
495
|
+
y_s = _reconstruct_y_from_slacks_and_gaps(
|
|
496
|
+
b,
|
|
497
|
+
b_n,
|
|
498
|
+
s,
|
|
499
|
+
d,
|
|
500
|
+
)
|
|
501
|
+
result = y_s[inv_perm_z]
|
|
502
|
+
if orig_dtype != jnp.float64:
|
|
503
|
+
result = result.astype(orig_dtype)
|
|
504
|
+
|
|
505
|
+
return result, (
|
|
506
|
+
perm_z,
|
|
507
|
+
inv_perm_z,
|
|
508
|
+
s,
|
|
509
|
+
d,
|
|
510
|
+
tau,
|
|
511
|
+
tau_m,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
def _solver_bwd(
|
|
515
|
+
residuals: tuple,
|
|
516
|
+
g: jax.Array,
|
|
517
|
+
) -> tuple[jax.Array, jax.Array, jax.Array]:
|
|
518
|
+
"""Backward pass for the entropic LP solver custom VJP."""
|
|
519
|
+
perm_z, inv_perm_z, s, d, tau, tau_m = residuals
|
|
520
|
+
orig_dtype = g.dtype
|
|
521
|
+
g = jnp.asarray(g)
|
|
522
|
+
_hp = _high_precision_dtype()
|
|
523
|
+
if g.dtype != _hp:
|
|
524
|
+
g = g.astype(_hp)
|
|
525
|
+
|
|
526
|
+
g_s = g[perm_z]
|
|
527
|
+
|
|
528
|
+
inv_s = 1.0 / s
|
|
529
|
+
inv_d = 1.0 / d
|
|
530
|
+
|
|
531
|
+
# Solve H_y*lam + mu*1 = g_s with 1^T*lam = 0.
|
|
532
|
+
# Substitute lam = D^T*eta (zero-mean since 1^T*D^T = 0)
|
|
533
|
+
# to get M*eta = D*g_s where
|
|
534
|
+
# M = diag(1/w_alpha) + L*diag(1/w_beta)*L.
|
|
535
|
+
w_alpha = s / tau
|
|
536
|
+
w_beta = d / tau_m
|
|
537
|
+
rhs = g_s[:-1] - g_s[1:] # D*g_s
|
|
538
|
+
|
|
539
|
+
def m_matvec(v: jax.Array) -> jax.Array:
|
|
540
|
+
"""Compute M*v for the CG linear solve."""
|
|
541
|
+
return _hessian_beta_matvec(
|
|
542
|
+
v,
|
|
543
|
+
1.0 / w_beta,
|
|
544
|
+
1.0 / w_alpha,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
eta, _ = jax.scipy.sparse.linalg.cg(
|
|
548
|
+
m_matvec,
|
|
549
|
+
rhs,
|
|
550
|
+
tol=tol,
|
|
551
|
+
maxiter=50,
|
|
552
|
+
)
|
|
553
|
+
lam = jnp.concatenate(
|
|
554
|
+
[eta[:1], eta[1:] - eta[:-1], -eta[-1:]],
|
|
555
|
+
) # D^T*eta
|
|
556
|
+
|
|
557
|
+
def _reverse_cumsum(u: jax.Array) -> jax.Array:
|
|
558
|
+
"""Compute reverse cumulative sum of u."""
|
|
559
|
+
return jnp.flip(jnp.cumsum(jnp.flip(u)))
|
|
560
|
+
|
|
561
|
+
def _at_matvec(u: jax.Array) -> jax.Array:
|
|
562
|
+
"""A^T u: (n-1,) -> (n,), prefix-sum matrix."""
|
|
563
|
+
return jnp.concatenate(
|
|
564
|
+
[
|
|
565
|
+
_reverse_cumsum(u),
|
|
566
|
+
jnp.zeros((1,), dtype=u.dtype),
|
|
567
|
+
],
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
def h_y_matvec(v: jax.Array) -> jax.Array:
|
|
571
|
+
"""H_y v = tau*A^T diag(1/s) A v + tau_m*D^T diag(1/d) D v."""
|
|
572
|
+
Av = jnp.cumsum(v)[:-1]
|
|
573
|
+
Dv = v[:-1] - v[1:]
|
|
574
|
+
return tau * _at_matvec(Av * inv_s) + tau_m * _diff_T(Dv * inv_d)
|
|
575
|
+
|
|
576
|
+
h_lam = h_y_matvec(lam)
|
|
577
|
+
mu = jnp.mean(g_s - h_lam)
|
|
578
|
+
|
|
579
|
+
# Gradients for (z, b, b_n) -- no chain-rule to w here;
|
|
580
|
+
# autodiff through _smooth_majorization_bounds handles that.
|
|
581
|
+
grad_z_s = lam
|
|
582
|
+
grad_b = tau * jnp.cumsum(lam)[:-1] * inv_s
|
|
583
|
+
grad_b_n = mu
|
|
584
|
+
|
|
585
|
+
grad_z = grad_z_s[inv_perm_z]
|
|
586
|
+
if orig_dtype != jnp.float64:
|
|
587
|
+
grad_z = grad_z.astype(orig_dtype)
|
|
588
|
+
grad_b = grad_b.astype(orig_dtype)
|
|
589
|
+
grad_b_n = grad_b_n.astype(orig_dtype)
|
|
590
|
+
|
|
591
|
+
return (grad_z, grad_b, grad_b_n)
|
|
592
|
+
|
|
593
|
+
_solver.defvjp(_solver_fwd, _solver_bwd)
|
|
594
|
+
|
|
595
|
+
# -- outer function: smooth bounds + solver --
|
|
596
|
+
|
|
597
|
+
def _proj_fn(z: jax.Array, w: jax.Array) -> jax.Array:
|
|
598
|
+
"""Project z onto the permutahedron of w using smooth bounds."""
|
|
599
|
+
n = z.shape[0]
|
|
600
|
+
if n <= 1:
|
|
601
|
+
return w
|
|
602
|
+
tau_bounds = jnp.asarray(bounds_softness, dtype=w.dtype)
|
|
603
|
+
b, b_n = _smooth_majorization_bounds(
|
|
604
|
+
w,
|
|
605
|
+
tau_bounds,
|
|
606
|
+
checkpointed,
|
|
607
|
+
)
|
|
608
|
+
return _solver(z, b, b_n)
|
|
609
|
+
|
|
610
|
+
return _proj_fn
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# -- c0: euclidean (q=2) ----------------------------------------------
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _pav_isotonic_decreasing_pnorm_q2(
|
|
617
|
+
y: jax.Array,
|
|
618
|
+
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
|
|
619
|
+
"""PAV isotonic regression for the q=2 (euclidean) case."""
|
|
620
|
+
y = jnp.asarray(y)
|
|
621
|
+
n = y.shape[0]
|
|
622
|
+
dtype = y.dtype
|
|
623
|
+
|
|
624
|
+
starts0 = jnp.full((n,), n, dtype=jnp.int32)
|
|
625
|
+
sums0 = jnp.zeros((n,), dtype=dtype)
|
|
626
|
+
lens0 = jnp.ones((n,), dtype=jnp.int32)
|
|
627
|
+
m0 = jnp.int32(0)
|
|
628
|
+
|
|
629
|
+
def merge_cond(
|
|
630
|
+
state: tuple,
|
|
631
|
+
) -> jax.Array:
|
|
632
|
+
"""Check whether the top two blocks violate isotonic order."""
|
|
633
|
+
starts, sums, lens, m = state
|
|
634
|
+
return (m >= 2) & (
|
|
635
|
+
(sums[m - 2] / lens[m - 2].astype(dtype)) < (sums[m - 1] / lens[m - 1].astype(dtype))
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
def merge_body(
|
|
639
|
+
state: tuple,
|
|
640
|
+
) -> tuple:
|
|
641
|
+
"""Merge the top two blocks on the stack."""
|
|
642
|
+
starts, sums, lens, m = state
|
|
643
|
+
i_prev = m - 2
|
|
644
|
+
i_top = m - 1
|
|
645
|
+
|
|
646
|
+
sums = sums.at[i_prev].set(
|
|
647
|
+
sums[i_prev] + sums[i_top],
|
|
648
|
+
)
|
|
649
|
+
lens = lens.at[i_prev].set(
|
|
650
|
+
lens[i_prev] + lens[i_top],
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
sums = sums.at[i_top].set(
|
|
654
|
+
jnp.array(0, dtype=dtype),
|
|
655
|
+
)
|
|
656
|
+
lens = lens.at[i_top].set(jnp.int32(1))
|
|
657
|
+
starts = starts.at[i_top].set(jnp.int32(n))
|
|
658
|
+
|
|
659
|
+
return (starts, sums, lens, m - 1)
|
|
660
|
+
|
|
661
|
+
def for_body(
|
|
662
|
+
i: jax.Array,
|
|
663
|
+
state: tuple,
|
|
664
|
+
) -> tuple:
|
|
665
|
+
"""Push element i onto the stack and merge violating blocks."""
|
|
666
|
+
starts, sums, lens, m = state
|
|
667
|
+
starts = starts.at[m].set(jnp.int32(i))
|
|
668
|
+
sums = sums.at[m].set(y[i])
|
|
669
|
+
lens = lens.at[m].set(jnp.int32(1))
|
|
670
|
+
m = m + 1
|
|
671
|
+
return lax.while_loop(
|
|
672
|
+
merge_cond,
|
|
673
|
+
merge_body,
|
|
674
|
+
(starts, sums, lens, m),
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
starts, sums, lens, m = lax.fori_loop(
|
|
678
|
+
0,
|
|
679
|
+
n,
|
|
680
|
+
for_body,
|
|
681
|
+
(starts0, sums0, lens0, m0),
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
idx = jnp.arange(n, dtype=jnp.int32)
|
|
685
|
+
starts = jnp.where(idx < m, starts, jnp.int32(n))
|
|
686
|
+
sums = jnp.where(
|
|
687
|
+
idx < m,
|
|
688
|
+
sums,
|
|
689
|
+
jnp.array(0, dtype=dtype),
|
|
690
|
+
)
|
|
691
|
+
lens = jnp.where(idx < m, lens, jnp.int32(1))
|
|
692
|
+
|
|
693
|
+
avgs = sums / lens.astype(dtype)
|
|
694
|
+
block_idx = jnp.searchsorted(starts, idx, side="right") - jnp.int32(1)
|
|
695
|
+
v = avgs[block_idx]
|
|
696
|
+
return v, block_idx, starts, lens
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@jax.custom_vjp
|
|
700
|
+
def _proj_permutahedron_pnorm_q2(
|
|
701
|
+
z: jax.Array,
|
|
702
|
+
w: jax.Array,
|
|
703
|
+
) -> jax.Array:
|
|
704
|
+
"""Project z onto the permutahedron of w using q=2 (euclidean)."""
|
|
705
|
+
z = jnp.asarray(z)
|
|
706
|
+
w = jnp.asarray(w)
|
|
707
|
+
|
|
708
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
709
|
+
z_sorted = z[perm_z]
|
|
710
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
711
|
+
|
|
712
|
+
perm_w = jnp.argsort(-w, stable=True)
|
|
713
|
+
w_sorted = w[perm_w]
|
|
714
|
+
|
|
715
|
+
y = z_sorted - w_sorted
|
|
716
|
+
v, _, _, _ = _pav_isotonic_decreasing_pnorm_q2(y)
|
|
717
|
+
|
|
718
|
+
p_sorted = z_sorted - v
|
|
719
|
+
return p_sorted[inv_perm_z]
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _proj_permutahedron_pnorm_q2_fwd(
|
|
723
|
+
z: jax.Array,
|
|
724
|
+
w: jax.Array,
|
|
725
|
+
) -> tuple[jax.Array, tuple]:
|
|
726
|
+
"""Forward pass for q=2 projection custom VJP."""
|
|
727
|
+
z = jnp.asarray(z)
|
|
728
|
+
w = jnp.asarray(w)
|
|
729
|
+
|
|
730
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
731
|
+
z_sorted = z[perm_z]
|
|
732
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
733
|
+
|
|
734
|
+
perm_w = jnp.argsort(-w, stable=True)
|
|
735
|
+
w_sorted = w[perm_w]
|
|
736
|
+
inv_perm_w = _inv_permutation(perm_w)
|
|
737
|
+
|
|
738
|
+
y = z_sorted - w_sorted
|
|
739
|
+
v, block_idx, _, lens = _pav_isotonic_decreasing_pnorm_q2(y)
|
|
740
|
+
|
|
741
|
+
p_sorted = z_sorted - v
|
|
742
|
+
p = p_sorted[inv_perm_z]
|
|
743
|
+
|
|
744
|
+
aux = (
|
|
745
|
+
perm_z,
|
|
746
|
+
inv_perm_z,
|
|
747
|
+
perm_w,
|
|
748
|
+
inv_perm_w,
|
|
749
|
+
block_idx,
|
|
750
|
+
lens,
|
|
751
|
+
)
|
|
752
|
+
return p, aux
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _proj_permutahedron_pnorm_q2_bwd(
|
|
756
|
+
aux: tuple,
|
|
757
|
+
g: jax.Array,
|
|
758
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
759
|
+
"""Backward pass for q=2 projection custom VJP."""
|
|
760
|
+
(
|
|
761
|
+
perm_z,
|
|
762
|
+
inv_perm_z,
|
|
763
|
+
perm_w,
|
|
764
|
+
inv_perm_w,
|
|
765
|
+
block_idx,
|
|
766
|
+
lens,
|
|
767
|
+
) = aux
|
|
768
|
+
g = jnp.asarray(g)
|
|
769
|
+
dtype = g.dtype
|
|
770
|
+
n = g.shape[0]
|
|
771
|
+
|
|
772
|
+
g_sorted = g[perm_z]
|
|
773
|
+
block_sum_g = segment_sum(
|
|
774
|
+
g_sorted,
|
|
775
|
+
block_idx,
|
|
776
|
+
num_segments=n,
|
|
777
|
+
)
|
|
778
|
+
Jt_g = block_sum_g[block_idx] / lens[block_idx].astype(
|
|
779
|
+
dtype,
|
|
780
|
+
) # symmetric => same as J g
|
|
781
|
+
|
|
782
|
+
grad_z_sorted = g_sorted - Jt_g
|
|
783
|
+
grad_w_sorted = Jt_g
|
|
784
|
+
|
|
785
|
+
return (
|
|
786
|
+
grad_z_sorted[inv_perm_z],
|
|
787
|
+
grad_w_sorted[inv_perm_w],
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
_proj_permutahedron_pnorm_q2.defvjp(
|
|
792
|
+
_proj_permutahedron_pnorm_q2_fwd,
|
|
793
|
+
_proj_permutahedron_pnorm_q2_bwd,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
# -- c1: p-norm q=3 (p=3/2) -------------------------------------------
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _solve_block_gamma_q3(
|
|
801
|
+
s_sorted: jax.Array,
|
|
802
|
+
prefix_s: jax.Array,
|
|
803
|
+
prefix_s2: jax.Array,
|
|
804
|
+
start: jax.Array,
|
|
805
|
+
length: jax.Array,
|
|
806
|
+
sum_w: jax.Array,
|
|
807
|
+
min_s: jax.Array,
|
|
808
|
+
max_s: jax.Array,
|
|
809
|
+
) -> jax.Array:
|
|
810
|
+
r"""Analytical solver for sum_block (gamma - s)|gamma - s| + sum_w = 0.
|
|
811
|
+
|
|
812
|
+
The function g(gamma) is piecewise quadratic with breakpoints at
|
|
813
|
+
each s_i. For each possible split point k (separating elements
|
|
814
|
+
>= gamma from those < gamma), g is a quadratic a*gamma^2 + b*gamma
|
|
815
|
+
+ c whose coefficients are computed from prefix sums. We solve all
|
|
816
|
+
quadratics in parallel and select the root that falls in its valid
|
|
817
|
+
interval.
|
|
818
|
+
"""
|
|
819
|
+
dtype = s_sorted.dtype
|
|
820
|
+
n = s_sorted.shape[0]
|
|
821
|
+
end = start + length
|
|
822
|
+
|
|
823
|
+
# Possible split points k in {0, 1, ..., n}.
|
|
824
|
+
# Split k: elements [start, k) have s >= gamma,
|
|
825
|
+
# [k, end) have s < gamma.
|
|
826
|
+
ks = jnp.arange(n + 1)
|
|
827
|
+
|
|
828
|
+
# Quadratic coefficients g_k(gamma) = a*gamma^2 + b*gamma + c = 0
|
|
829
|
+
n_hi = (ks - start).astype(dtype)
|
|
830
|
+
n_lo = (end - ks).astype(dtype)
|
|
831
|
+
S_hi = prefix_s[ks] - prefix_s[start]
|
|
832
|
+
S_lo = prefix_s[end] - prefix_s[ks]
|
|
833
|
+
M2_hi = prefix_s2[ks] - prefix_s2[start]
|
|
834
|
+
M2_lo = prefix_s2[end] - prefix_s2[ks]
|
|
835
|
+
|
|
836
|
+
a = n_lo - n_hi
|
|
837
|
+
b = 2.0 * (S_hi - S_lo)
|
|
838
|
+
c = (M2_lo - M2_hi) + sum_w
|
|
839
|
+
|
|
840
|
+
# The ascending zero-crossing is always at
|
|
841
|
+
# (-b + sqrt(disc)) / (2a).
|
|
842
|
+
disc = b * b - 4.0 * a * c
|
|
843
|
+
sqrt_disc = jnp.sqrt(jnp.maximum(disc, 0.0))
|
|
844
|
+
a_safe = jnp.where(a != 0.0, a, 1.0)
|
|
845
|
+
b_safe = jnp.where(b != 0.0, b, 1.0)
|
|
846
|
+
gamma_quad = (-b + sqrt_disc) / (2.0 * a_safe)
|
|
847
|
+
gamma_lin = -c / b_safe
|
|
848
|
+
gamma_k = jnp.where(a != 0.0, gamma_quad, gamma_lin)
|
|
849
|
+
|
|
850
|
+
# Validity interval (s sorted descending):
|
|
851
|
+
# k = start: gamma > s[start], upper = +inf
|
|
852
|
+
# start < k < end: s[k] < gamma <= s[k-1]
|
|
853
|
+
# k = end: lower = -inf, gamma <= s[end-1]
|
|
854
|
+
s_at_k = s_sorted[jnp.clip(ks, 0, n - 1)]
|
|
855
|
+
s_at_km1 = s_sorted[jnp.clip(ks - 1, 0, n - 1)]
|
|
856
|
+
lo_bound = jnp.where(ks < end, s_at_k, -jnp.inf)
|
|
857
|
+
hi_bound = jnp.where(ks > start, s_at_km1, jnp.inf)
|
|
858
|
+
|
|
859
|
+
eps = jnp.finfo(dtype).eps ** 0.75
|
|
860
|
+
valid = (
|
|
861
|
+
(ks >= start)
|
|
862
|
+
& (ks <= end)
|
|
863
|
+
& (gamma_k > lo_bound - eps)
|
|
864
|
+
& (gamma_k <= hi_bound + eps)
|
|
865
|
+
& (disc >= -eps)
|
|
866
|
+
& ((a != 0.0) | (b != 0.0))
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
best = jnp.argmax(valid)
|
|
870
|
+
result = gamma_k[best]
|
|
871
|
+
return jnp.where(
|
|
872
|
+
jnp.any(valid),
|
|
873
|
+
result,
|
|
874
|
+
(min_s + max_s) * 0.5,
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _pav_isotonic_decreasing_pnorm_q3(
|
|
879
|
+
s: jax.Array,
|
|
880
|
+
w: jax.Array,
|
|
881
|
+
) -> tuple[jax.Array, jax.Array, jax.Array]:
|
|
882
|
+
"""PAV isotonic regression for the q=3 (p=3/2) case."""
|
|
883
|
+
s = jnp.asarray(s)
|
|
884
|
+
w = jnp.asarray(w)
|
|
885
|
+
n = s.shape[0]
|
|
886
|
+
dtype = s.dtype
|
|
887
|
+
|
|
888
|
+
_zero = jnp.zeros((1,), dtype=dtype)
|
|
889
|
+
prefix_s = jnp.concatenate([_zero, jnp.cumsum(s)])
|
|
890
|
+
prefix_s2 = jnp.concatenate([_zero, jnp.cumsum(s * s)])
|
|
891
|
+
|
|
892
|
+
starts0 = jnp.full((n,), n, dtype=jnp.int32)
|
|
893
|
+
lens0 = jnp.ones((n,), dtype=jnp.int32)
|
|
894
|
+
sumw0 = jnp.zeros((n,), dtype=dtype)
|
|
895
|
+
mins0 = jnp.full((n,), jnp.inf, dtype=dtype)
|
|
896
|
+
maxs0 = jnp.full((n,), -jnp.inf, dtype=dtype)
|
|
897
|
+
gam0 = jnp.zeros((n,), dtype=dtype)
|
|
898
|
+
m0 = jnp.int32(0)
|
|
899
|
+
|
|
900
|
+
def merge_cond(state: tuple) -> jax.Array:
|
|
901
|
+
"""Check whether the top two q=3 blocks violate isotonic order."""
|
|
902
|
+
starts, lens, sumw, mins, maxs, gam, m = state
|
|
903
|
+
return (m >= 2) & (gam[m - 2] < gam[m - 1])
|
|
904
|
+
|
|
905
|
+
def merge_body(state: tuple) -> tuple:
|
|
906
|
+
"""Merge the top two q=3 blocks on the stack."""
|
|
907
|
+
starts, lens, sumw, mins, maxs, gam, m = state
|
|
908
|
+
i_prev = m - 2
|
|
909
|
+
i_top = m - 1
|
|
910
|
+
|
|
911
|
+
lens_new = lens[i_prev] + lens[i_top]
|
|
912
|
+
sumw_new = sumw[i_prev] + sumw[i_top]
|
|
913
|
+
mins_new = jnp.minimum(mins[i_prev], mins[i_top])
|
|
914
|
+
maxs_new = jnp.maximum(maxs[i_prev], maxs[i_top])
|
|
915
|
+
|
|
916
|
+
gam_new = _solve_block_gamma_q3(
|
|
917
|
+
s,
|
|
918
|
+
prefix_s,
|
|
919
|
+
prefix_s2,
|
|
920
|
+
starts[i_prev],
|
|
921
|
+
lens_new,
|
|
922
|
+
sumw_new,
|
|
923
|
+
mins_new,
|
|
924
|
+
maxs_new,
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
lens = lens.at[i_prev].set(lens_new)
|
|
928
|
+
sumw = sumw.at[i_prev].set(sumw_new)
|
|
929
|
+
mins = mins.at[i_prev].set(mins_new)
|
|
930
|
+
maxs = maxs.at[i_prev].set(maxs_new)
|
|
931
|
+
gam = gam.at[i_prev].set(gam_new)
|
|
932
|
+
|
|
933
|
+
starts = starts.at[i_top].set(jnp.int32(n))
|
|
934
|
+
lens = lens.at[i_top].set(jnp.int32(1))
|
|
935
|
+
sumw = sumw.at[i_top].set(
|
|
936
|
+
jnp.array(0, dtype=dtype),
|
|
937
|
+
)
|
|
938
|
+
mins = mins.at[i_top].set(
|
|
939
|
+
jnp.array(jnp.inf, dtype=dtype),
|
|
940
|
+
)
|
|
941
|
+
maxs = maxs.at[i_top].set(
|
|
942
|
+
jnp.array(-jnp.inf, dtype=dtype),
|
|
943
|
+
)
|
|
944
|
+
gam = gam.at[i_top].set(
|
|
945
|
+
jnp.array(0, dtype=dtype),
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
return (
|
|
949
|
+
starts,
|
|
950
|
+
lens,
|
|
951
|
+
sumw,
|
|
952
|
+
mins,
|
|
953
|
+
maxs,
|
|
954
|
+
gam,
|
|
955
|
+
m - 1,
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
def for_body(i: jax.Array, state: tuple) -> tuple:
|
|
959
|
+
"""Push element i onto the q=3 stack and merge violating blocks."""
|
|
960
|
+
starts, lens, sumw, mins, maxs, gam, m = state
|
|
961
|
+
|
|
962
|
+
si = s[i]
|
|
963
|
+
wi = w[i]
|
|
964
|
+
starts = starts.at[m].set(jnp.int32(i))
|
|
965
|
+
lens = lens.at[m].set(jnp.int32(1))
|
|
966
|
+
sumw = sumw.at[m].set(wi)
|
|
967
|
+
mins = mins.at[m].set(si)
|
|
968
|
+
maxs = maxs.at[m].set(si)
|
|
969
|
+
|
|
970
|
+
gam_i = _solve_block_gamma_q3(
|
|
971
|
+
s,
|
|
972
|
+
prefix_s,
|
|
973
|
+
prefix_s2,
|
|
974
|
+
i,
|
|
975
|
+
jnp.int32(1),
|
|
976
|
+
wi,
|
|
977
|
+
si,
|
|
978
|
+
si,
|
|
979
|
+
)
|
|
980
|
+
gam = gam.at[m].set(gam_i)
|
|
981
|
+
|
|
982
|
+
m = m + 1
|
|
983
|
+
return lax.while_loop(
|
|
984
|
+
merge_cond,
|
|
985
|
+
merge_body,
|
|
986
|
+
(starts, lens, sumw, mins, maxs, gam, m),
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
starts, lens, sumw, mins, maxs, gam, m = lax.fori_loop(
|
|
990
|
+
0,
|
|
991
|
+
n,
|
|
992
|
+
for_body,
|
|
993
|
+
(starts0, lens0, sumw0, mins0, maxs0, gam0, m0),
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
idx = jnp.arange(n, dtype=jnp.int32)
|
|
997
|
+
starts = jnp.where(idx < m, starts, jnp.int32(n))
|
|
998
|
+
lens = jnp.where(idx < m, lens, jnp.int32(1))
|
|
999
|
+
gam = jnp.where(
|
|
1000
|
+
idx < m,
|
|
1001
|
+
gam,
|
|
1002
|
+
jnp.array(0, dtype=dtype),
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
block_idx = jnp.searchsorted(starts, idx, side="right") - jnp.int32(1)
|
|
1006
|
+
v = gam[block_idx]
|
|
1007
|
+
return v, block_idx, lens
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
@jax.custom_vjp
|
|
1011
|
+
def _proj_permutahedron_pnorm_q3(
|
|
1012
|
+
z: jax.Array,
|
|
1013
|
+
w: jax.Array,
|
|
1014
|
+
) -> jax.Array:
|
|
1015
|
+
"""Project z onto the permutahedron of w using q=3 (p=3/2)."""
|
|
1016
|
+
z = jnp.asarray(z)
|
|
1017
|
+
w = jnp.asarray(w)
|
|
1018
|
+
orig_dtype = z.dtype
|
|
1019
|
+
_hp = _high_precision_dtype()
|
|
1020
|
+
z, w = z.astype(_hp), w.astype(_hp)
|
|
1021
|
+
|
|
1022
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
1023
|
+
z_sorted = z[perm_z]
|
|
1024
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
1025
|
+
|
|
1026
|
+
perm_w = jnp.argsort(-w, stable=True)
|
|
1027
|
+
w_sorted = w[perm_w]
|
|
1028
|
+
|
|
1029
|
+
v, block_idx, lens = _pav_isotonic_decreasing_pnorm_q3(
|
|
1030
|
+
z_sorted,
|
|
1031
|
+
w_sorted,
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
t = z_sorted - v
|
|
1035
|
+
# q=3 => nabla R^*(t) = t|t|^{q-2} = t|t|
|
|
1036
|
+
y_sorted = t * jnp.abs(t)
|
|
1037
|
+
|
|
1038
|
+
return y_sorted[inv_perm_z].astype(orig_dtype)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _proj_permutahedron_pnorm_q3_fwd(
|
|
1042
|
+
z: jax.Array,
|
|
1043
|
+
w: jax.Array,
|
|
1044
|
+
) -> tuple[jax.Array, tuple]:
|
|
1045
|
+
"""Forward pass for q=3 projection custom VJP."""
|
|
1046
|
+
z = jnp.asarray(z)
|
|
1047
|
+
w = jnp.asarray(w)
|
|
1048
|
+
orig_dtype = z.dtype
|
|
1049
|
+
_hp = _high_precision_dtype()
|
|
1050
|
+
z, w = z.astype(_hp), w.astype(_hp)
|
|
1051
|
+
|
|
1052
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
1053
|
+
z_sorted = z[perm_z]
|
|
1054
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
1055
|
+
|
|
1056
|
+
perm_w = jnp.argsort(-w, stable=True)
|
|
1057
|
+
w_sorted = w[perm_w]
|
|
1058
|
+
inv_perm_w = _inv_permutation(perm_w)
|
|
1059
|
+
|
|
1060
|
+
v, block_idx, lens = _pav_isotonic_decreasing_pnorm_q3(
|
|
1061
|
+
z_sorted,
|
|
1062
|
+
w_sorted,
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
t = z_sorted - v
|
|
1066
|
+
y_sorted = t * jnp.abs(t)
|
|
1067
|
+
y = y_sorted[inv_perm_z].astype(orig_dtype)
|
|
1068
|
+
|
|
1069
|
+
aux = (
|
|
1070
|
+
perm_z,
|
|
1071
|
+
inv_perm_z,
|
|
1072
|
+
perm_w,
|
|
1073
|
+
inv_perm_w,
|
|
1074
|
+
block_idx,
|
|
1075
|
+
lens,
|
|
1076
|
+
t,
|
|
1077
|
+
y,
|
|
1078
|
+
)
|
|
1079
|
+
return y, aux
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _proj_permutahedron_pnorm_q3_bwd(
|
|
1083
|
+
aux: tuple,
|
|
1084
|
+
g: jax.Array,
|
|
1085
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
1086
|
+
"""Backward pass for q=3 projection custom VJP."""
|
|
1087
|
+
(
|
|
1088
|
+
perm_z,
|
|
1089
|
+
inv_perm_z,
|
|
1090
|
+
perm_w,
|
|
1091
|
+
inv_perm_w,
|
|
1092
|
+
block_idx,
|
|
1093
|
+
lens,
|
|
1094
|
+
t,
|
|
1095
|
+
y,
|
|
1096
|
+
) = aux
|
|
1097
|
+
orig_dtype = g.dtype
|
|
1098
|
+
_hp = _high_precision_dtype()
|
|
1099
|
+
g = jnp.asarray(g).astype(_hp)
|
|
1100
|
+
n = g.shape[0]
|
|
1101
|
+
dtype = _hp
|
|
1102
|
+
|
|
1103
|
+
# upstream grad on y_sorted
|
|
1104
|
+
g_sorted_y = g[perm_z]
|
|
1105
|
+
|
|
1106
|
+
# y = t|t| => dy/dt = 2|t|
|
|
1107
|
+
g_t = g_sorted_y * (2.0 * jnp.abs(t))
|
|
1108
|
+
|
|
1109
|
+
# weights for (d v / d s)^T g_t : alpha_i propto |t_i|^{q-2} = |t_i|
|
|
1110
|
+
weight = jnp.abs(t)
|
|
1111
|
+
denom_block = segment_sum(
|
|
1112
|
+
weight,
|
|
1113
|
+
block_idx,
|
|
1114
|
+
num_segments=n,
|
|
1115
|
+
)
|
|
1116
|
+
denom = denom_block[block_idx]
|
|
1117
|
+
|
|
1118
|
+
sumg_block = segment_sum(
|
|
1119
|
+
g_t,
|
|
1120
|
+
block_idx,
|
|
1121
|
+
num_segments=n,
|
|
1122
|
+
)
|
|
1123
|
+
sumg = sumg_block[block_idx]
|
|
1124
|
+
|
|
1125
|
+
lens_elem = lens[block_idx].astype(dtype)
|
|
1126
|
+
alpha = jnp.where(
|
|
1127
|
+
denom > 0,
|
|
1128
|
+
weight / denom,
|
|
1129
|
+
jnp.array(1.0, dtype=dtype) / lens_elem,
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
jtg_s = alpha * sumg
|
|
1133
|
+
grad_z_sorted = g_t - jtg_s
|
|
1134
|
+
|
|
1135
|
+
# d gamma/dw_k = -1/(2 sum |t|)
|
|
1136
|
+
# => dt/dw_k = +1/(2 sum |t|)
|
|
1137
|
+
grad_w_sorted = jnp.where(
|
|
1138
|
+
denom > 0,
|
|
1139
|
+
sumg / (2.0 * denom),
|
|
1140
|
+
jnp.array(0.0, dtype=dtype),
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
return (
|
|
1144
|
+
grad_z_sorted[inv_perm_z].astype(orig_dtype),
|
|
1145
|
+
grad_w_sorted[inv_perm_w].astype(orig_dtype),
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
_proj_permutahedron_pnorm_q3.defvjp(
|
|
1150
|
+
_proj_permutahedron_pnorm_q3_fwd,
|
|
1151
|
+
_proj_permutahedron_pnorm_q3_bwd,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
# -- c2: p-norm q=4 (p=4/3) -------------------------------------------
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def _solve_block_gamma_q4(
|
|
1159
|
+
len_b: jax.Array,
|
|
1160
|
+
sum_w: jax.Array,
|
|
1161
|
+
m1: jax.Array,
|
|
1162
|
+
m2: jax.Array,
|
|
1163
|
+
m3: jax.Array,
|
|
1164
|
+
min_s: jax.Array,
|
|
1165
|
+
max_s: jax.Array,
|
|
1166
|
+
) -> jax.Array:
|
|
1167
|
+
r"""Closed-form solver for the q=4 block gamma.
|
|
1168
|
+
|
|
1169
|
+
Solves sum_block (gamma - s)^3 + sum_w = 0 via Cardano's
|
|
1170
|
+
hyperbolic method. In shifted form u = gamma - c, this is the
|
|
1171
|
+
depressed cubic u^3 + pu + q = 0 with p = 3*mu_2/n >= 0, which
|
|
1172
|
+
always has exactly one real root.
|
|
1173
|
+
"""
|
|
1174
|
+
dtype = m1.dtype
|
|
1175
|
+
len_f = len_b.astype(dtype)
|
|
1176
|
+
c = m1 / len_f # mean of s values in block
|
|
1177
|
+
# Central moments: mu_k = sum (s_i - c)^k
|
|
1178
|
+
mu2 = m2 - 2.0 * c * m1 + len_f * c * c
|
|
1179
|
+
mu3 = m3 - 3.0 * c * m2 + 3.0 * c * c * m1 - len_f * c**3
|
|
1180
|
+
|
|
1181
|
+
# Depressed cubic: u^3 + p*u + q = 0
|
|
1182
|
+
p = 3.0 * mu2 / len_f # >= 0 (sum of squares)
|
|
1183
|
+
q = (sum_w - mu3) / len_f
|
|
1184
|
+
|
|
1185
|
+
# Hyperbolic Cardano:
|
|
1186
|
+
# u = -sign(q) * 2*sqrt(p/3)
|
|
1187
|
+
# * sinh(arcsinh(A)/3)
|
|
1188
|
+
# where A = 3|q| / (2*p*sqrt(p/3))
|
|
1189
|
+
sp3 = jnp.sqrt(jnp.maximum(p / 3.0, 0.0)) # sqrt(p/3)
|
|
1190
|
+
denom = (
|
|
1191
|
+
2.0
|
|
1192
|
+
* jnp.maximum(
|
|
1193
|
+
p,
|
|
1194
|
+
jnp.finfo(dtype).tiny,
|
|
1195
|
+
)
|
|
1196
|
+
* sp3
|
|
1197
|
+
)
|
|
1198
|
+
A = 3.0 * jnp.abs(q) / denom
|
|
1199
|
+
u_hyp = -jnp.sign(q) * 2.0 * sp3 * jnp.sinh(jnp.arcsinh(A) / 3.0)
|
|
1200
|
+
|
|
1201
|
+
# When p ~ 0 (all s equal): u^3 + q = 0 => u = cbrt(-q)
|
|
1202
|
+
u_cbrt = -jnp.sign(q) * jnp.abs(q) ** (1.0 / 3.0)
|
|
1203
|
+
|
|
1204
|
+
u = jnp.where(
|
|
1205
|
+
p
|
|
1206
|
+
> jnp.finfo(dtype).eps
|
|
1207
|
+
* jnp.maximum(
|
|
1208
|
+
jnp.abs(q),
|
|
1209
|
+
1.0,
|
|
1210
|
+
),
|
|
1211
|
+
u_hyp,
|
|
1212
|
+
u_cbrt,
|
|
1213
|
+
)
|
|
1214
|
+
return u + c
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def _pav_isotonic_decreasing_pnorm_q4(
|
|
1218
|
+
s: jax.Array,
|
|
1219
|
+
w: jax.Array,
|
|
1220
|
+
) -> tuple[jax.Array, jax.Array, jax.Array]:
|
|
1221
|
+
"""PAV isotonic regression for the q=4 (p=4/3) case."""
|
|
1222
|
+
s = jnp.asarray(s)
|
|
1223
|
+
w = jnp.asarray(w)
|
|
1224
|
+
n = s.shape[0]
|
|
1225
|
+
dtype = s.dtype
|
|
1226
|
+
|
|
1227
|
+
starts0 = jnp.full((n,), n, dtype=jnp.int32)
|
|
1228
|
+
lens0 = jnp.ones((n,), dtype=jnp.int32)
|
|
1229
|
+
sumw0 = jnp.zeros((n,), dtype=dtype)
|
|
1230
|
+
m10 = jnp.zeros((n,), dtype=dtype)
|
|
1231
|
+
m20 = jnp.zeros((n,), dtype=dtype)
|
|
1232
|
+
m30 = jnp.zeros((n,), dtype=dtype)
|
|
1233
|
+
mins0 = jnp.full((n,), jnp.inf, dtype=dtype)
|
|
1234
|
+
maxs0 = jnp.full((n,), -jnp.inf, dtype=dtype)
|
|
1235
|
+
gam0 = jnp.zeros((n,), dtype=dtype)
|
|
1236
|
+
m0 = jnp.int32(0)
|
|
1237
|
+
|
|
1238
|
+
def merge_cond(state: tuple) -> jax.Array:
|
|
1239
|
+
"""Check whether the top two q=4 blocks violate isotonic order."""
|
|
1240
|
+
starts, lens, sumw, m1, m2, m3, mins, maxs, gam, m = state
|
|
1241
|
+
return (m >= 2) & (gam[m - 2] < gam[m - 1])
|
|
1242
|
+
|
|
1243
|
+
def merge_body(state: tuple) -> tuple:
|
|
1244
|
+
"""Merge the top two q=4 blocks on the stack."""
|
|
1245
|
+
starts, lens, sumw, m1, m2, m3, mins, maxs, gam, m = state
|
|
1246
|
+
i_prev = m - 2
|
|
1247
|
+
i_top = m - 1
|
|
1248
|
+
|
|
1249
|
+
lens_new = lens[i_prev] + lens[i_top]
|
|
1250
|
+
sumw_new = sumw[i_prev] + sumw[i_top]
|
|
1251
|
+
m1_new = m1[i_prev] + m1[i_top]
|
|
1252
|
+
m2_new = m2[i_prev] + m2[i_top]
|
|
1253
|
+
m3_new = m3[i_prev] + m3[i_top]
|
|
1254
|
+
mins_new = jnp.minimum(mins[i_prev], mins[i_top])
|
|
1255
|
+
maxs_new = jnp.maximum(maxs[i_prev], maxs[i_top])
|
|
1256
|
+
|
|
1257
|
+
gam_new = _solve_block_gamma_q4(
|
|
1258
|
+
lens_new,
|
|
1259
|
+
sumw_new,
|
|
1260
|
+
m1_new,
|
|
1261
|
+
m2_new,
|
|
1262
|
+
m3_new,
|
|
1263
|
+
mins_new,
|
|
1264
|
+
maxs_new,
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
lens = lens.at[i_prev].set(lens_new)
|
|
1268
|
+
sumw = sumw.at[i_prev].set(sumw_new)
|
|
1269
|
+
m1 = m1.at[i_prev].set(m1_new)
|
|
1270
|
+
m2 = m2.at[i_prev].set(m2_new)
|
|
1271
|
+
m3 = m3.at[i_prev].set(m3_new)
|
|
1272
|
+
mins = mins.at[i_prev].set(mins_new)
|
|
1273
|
+
maxs = maxs.at[i_prev].set(maxs_new)
|
|
1274
|
+
gam = gam.at[i_prev].set(gam_new)
|
|
1275
|
+
|
|
1276
|
+
starts = starts.at[i_top].set(jnp.int32(n))
|
|
1277
|
+
lens = lens.at[i_top].set(jnp.int32(1))
|
|
1278
|
+
sumw = sumw.at[i_top].set(
|
|
1279
|
+
jnp.array(0, dtype=dtype),
|
|
1280
|
+
)
|
|
1281
|
+
m1 = m1.at[i_top].set(
|
|
1282
|
+
jnp.array(0, dtype=dtype),
|
|
1283
|
+
)
|
|
1284
|
+
m2 = m2.at[i_top].set(
|
|
1285
|
+
jnp.array(0, dtype=dtype),
|
|
1286
|
+
)
|
|
1287
|
+
m3 = m3.at[i_top].set(
|
|
1288
|
+
jnp.array(0, dtype=dtype),
|
|
1289
|
+
)
|
|
1290
|
+
mins = mins.at[i_top].set(
|
|
1291
|
+
jnp.array(jnp.inf, dtype=dtype),
|
|
1292
|
+
)
|
|
1293
|
+
maxs = maxs.at[i_top].set(
|
|
1294
|
+
jnp.array(-jnp.inf, dtype=dtype),
|
|
1295
|
+
)
|
|
1296
|
+
gam = gam.at[i_top].set(
|
|
1297
|
+
jnp.array(0, dtype=dtype),
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
return (
|
|
1301
|
+
starts,
|
|
1302
|
+
lens,
|
|
1303
|
+
sumw,
|
|
1304
|
+
m1,
|
|
1305
|
+
m2,
|
|
1306
|
+
m3,
|
|
1307
|
+
mins,
|
|
1308
|
+
maxs,
|
|
1309
|
+
gam,
|
|
1310
|
+
m - 1,
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
def for_body(i: jax.Array, state: tuple) -> tuple:
|
|
1314
|
+
"""Push element i onto the q=4 stack and merge violating blocks."""
|
|
1315
|
+
starts, lens, sumw, m1, m2, m3, mins, maxs, gam, m = state
|
|
1316
|
+
|
|
1317
|
+
si = s[i]
|
|
1318
|
+
wi = w[i]
|
|
1319
|
+
starts = starts.at[m].set(jnp.int32(i))
|
|
1320
|
+
lens = lens.at[m].set(jnp.int32(1))
|
|
1321
|
+
sumw = sumw.at[m].set(wi)
|
|
1322
|
+
m1 = m1.at[m].set(si)
|
|
1323
|
+
m2 = m2.at[m].set(si * si)
|
|
1324
|
+
m3 = m3.at[m].set(si * si * si)
|
|
1325
|
+
mins = mins.at[m].set(si)
|
|
1326
|
+
maxs = maxs.at[m].set(si)
|
|
1327
|
+
|
|
1328
|
+
gam_i = _solve_block_gamma_q4(
|
|
1329
|
+
jnp.int32(1),
|
|
1330
|
+
wi,
|
|
1331
|
+
si,
|
|
1332
|
+
si * si,
|
|
1333
|
+
si * si * si,
|
|
1334
|
+
si,
|
|
1335
|
+
si,
|
|
1336
|
+
)
|
|
1337
|
+
gam = gam.at[m].set(gam_i)
|
|
1338
|
+
|
|
1339
|
+
m = m + 1
|
|
1340
|
+
return lax.while_loop(
|
|
1341
|
+
merge_cond,
|
|
1342
|
+
merge_body,
|
|
1343
|
+
(
|
|
1344
|
+
starts,
|
|
1345
|
+
lens,
|
|
1346
|
+
sumw,
|
|
1347
|
+
m1,
|
|
1348
|
+
m2,
|
|
1349
|
+
m3,
|
|
1350
|
+
mins,
|
|
1351
|
+
maxs,
|
|
1352
|
+
gam,
|
|
1353
|
+
m,
|
|
1354
|
+
),
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
(
|
|
1358
|
+
starts,
|
|
1359
|
+
lens,
|
|
1360
|
+
sumw,
|
|
1361
|
+
m1,
|
|
1362
|
+
m2,
|
|
1363
|
+
m3,
|
|
1364
|
+
mins,
|
|
1365
|
+
maxs,
|
|
1366
|
+
gam,
|
|
1367
|
+
m,
|
|
1368
|
+
) = lax.fori_loop(
|
|
1369
|
+
0,
|
|
1370
|
+
n,
|
|
1371
|
+
for_body,
|
|
1372
|
+
(
|
|
1373
|
+
starts0,
|
|
1374
|
+
lens0,
|
|
1375
|
+
sumw0,
|
|
1376
|
+
m10,
|
|
1377
|
+
m20,
|
|
1378
|
+
m30,
|
|
1379
|
+
mins0,
|
|
1380
|
+
maxs0,
|
|
1381
|
+
gam0,
|
|
1382
|
+
m0,
|
|
1383
|
+
),
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
idx = jnp.arange(n, dtype=jnp.int32)
|
|
1387
|
+
starts = jnp.where(idx < m, starts, jnp.int32(n))
|
|
1388
|
+
lens = jnp.where(idx < m, lens, jnp.int32(1))
|
|
1389
|
+
gam = jnp.where(
|
|
1390
|
+
idx < m,
|
|
1391
|
+
gam,
|
|
1392
|
+
jnp.array(0, dtype=dtype),
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
block_idx = jnp.searchsorted(starts, idx, side="right") - jnp.int32(1)
|
|
1396
|
+
v = gam[block_idx]
|
|
1397
|
+
return v, block_idx, lens
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
@jax.custom_vjp
|
|
1401
|
+
def _proj_permutahedron_pnorm_q4(
|
|
1402
|
+
z: jax.Array,
|
|
1403
|
+
w: jax.Array,
|
|
1404
|
+
) -> jax.Array:
|
|
1405
|
+
"""Project z onto the permutahedron of w using q=4 (p=4/3)."""
|
|
1406
|
+
z = jnp.asarray(z)
|
|
1407
|
+
w = jnp.asarray(w)
|
|
1408
|
+
orig_dtype = z.dtype
|
|
1409
|
+
_hp = _high_precision_dtype()
|
|
1410
|
+
z, w = z.astype(_hp), w.astype(_hp)
|
|
1411
|
+
|
|
1412
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
1413
|
+
z_sorted = z[perm_z]
|
|
1414
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
1415
|
+
|
|
1416
|
+
perm_w = jnp.argsort(-w, stable=True)
|
|
1417
|
+
w_sorted = w[perm_w]
|
|
1418
|
+
v, block_idx, lens = _pav_isotonic_decreasing_pnorm_q4(
|
|
1419
|
+
z_sorted,
|
|
1420
|
+
w_sorted,
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
t = z_sorted - v # (n,)
|
|
1424
|
+
# q=4 => nabla R^*(t) = t|t|^{q-2} = t^3
|
|
1425
|
+
y_sorted = t * (jnp.abs(t) ** 2)
|
|
1426
|
+
|
|
1427
|
+
return y_sorted[inv_perm_z].astype(orig_dtype)
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def _proj_permutahedron_pnorm_q4_fwd(
|
|
1431
|
+
z: jax.Array,
|
|
1432
|
+
w: jax.Array,
|
|
1433
|
+
) -> tuple[jax.Array, tuple]:
|
|
1434
|
+
"""Forward pass for q=4 projection custom VJP."""
|
|
1435
|
+
z = jnp.asarray(z)
|
|
1436
|
+
w = jnp.asarray(w)
|
|
1437
|
+
orig_dtype = z.dtype
|
|
1438
|
+
_hp = _high_precision_dtype()
|
|
1439
|
+
z, w = z.astype(_hp), w.astype(_hp)
|
|
1440
|
+
|
|
1441
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
1442
|
+
z_sorted = z[perm_z]
|
|
1443
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
1444
|
+
|
|
1445
|
+
perm_w = jnp.argsort(-w, stable=True)
|
|
1446
|
+
w_sorted = w[perm_w]
|
|
1447
|
+
inv_perm_w = _inv_permutation(perm_w)
|
|
1448
|
+
|
|
1449
|
+
v, block_idx, lens = _pav_isotonic_decreasing_pnorm_q4(
|
|
1450
|
+
z_sorted,
|
|
1451
|
+
w_sorted,
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
t = z_sorted - v
|
|
1455
|
+
y_sorted = t * (jnp.abs(t) ** 2)
|
|
1456
|
+
y = y_sorted[inv_perm_z].astype(orig_dtype)
|
|
1457
|
+
|
|
1458
|
+
aux = (
|
|
1459
|
+
perm_z,
|
|
1460
|
+
inv_perm_z,
|
|
1461
|
+
perm_w,
|
|
1462
|
+
inv_perm_w,
|
|
1463
|
+
block_idx,
|
|
1464
|
+
lens,
|
|
1465
|
+
t,
|
|
1466
|
+
y,
|
|
1467
|
+
)
|
|
1468
|
+
return y, aux
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
def _proj_permutahedron_pnorm_q4_bwd(
|
|
1472
|
+
aux: tuple,
|
|
1473
|
+
g: jax.Array,
|
|
1474
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
1475
|
+
"""Backward pass for q=4 projection custom VJP."""
|
|
1476
|
+
(
|
|
1477
|
+
perm_z,
|
|
1478
|
+
inv_perm_z,
|
|
1479
|
+
perm_w,
|
|
1480
|
+
inv_perm_w,
|
|
1481
|
+
block_idx,
|
|
1482
|
+
lens,
|
|
1483
|
+
t,
|
|
1484
|
+
y,
|
|
1485
|
+
) = aux
|
|
1486
|
+
orig_dtype = g.dtype
|
|
1487
|
+
_hp = _high_precision_dtype()
|
|
1488
|
+
g = jnp.asarray(g).astype(_hp)
|
|
1489
|
+
n = g.shape[0]
|
|
1490
|
+
dtype = _hp
|
|
1491
|
+
|
|
1492
|
+
# upstream grad on y_sorted
|
|
1493
|
+
g_sorted_y = g[perm_z]
|
|
1494
|
+
|
|
1495
|
+
# y = t^3 => dy/dt = 3 t^2
|
|
1496
|
+
g_t = g_sorted_y * (3.0 * (t * t))
|
|
1497
|
+
|
|
1498
|
+
# weights for (d v / d s)^T g_t :
|
|
1499
|
+
# alpha_i propto |t_i|^{q-2} = |t_i|^2
|
|
1500
|
+
weight = jnp.abs(t) ** 2 # (n,)
|
|
1501
|
+
denom_block = segment_sum(
|
|
1502
|
+
weight,
|
|
1503
|
+
block_idx,
|
|
1504
|
+
num_segments=n,
|
|
1505
|
+
)
|
|
1506
|
+
denom = denom_block[block_idx]
|
|
1507
|
+
|
|
1508
|
+
sumg_block = segment_sum(
|
|
1509
|
+
g_t,
|
|
1510
|
+
block_idx,
|
|
1511
|
+
num_segments=n,
|
|
1512
|
+
)
|
|
1513
|
+
sumg = sumg_block[block_idx]
|
|
1514
|
+
|
|
1515
|
+
lens_elem = lens[block_idx].astype(dtype)
|
|
1516
|
+
alpha = jnp.where(
|
|
1517
|
+
denom > 0,
|
|
1518
|
+
weight / denom,
|
|
1519
|
+
jnp.array(1.0, dtype=dtype) / lens_elem,
|
|
1520
|
+
)
|
|
1521
|
+
|
|
1522
|
+
jtg_s = alpha * sumg
|
|
1523
|
+
grad_z_sorted = g_t - jtg_s
|
|
1524
|
+
|
|
1525
|
+
# d gamma/dw_k = -1/(3 sum |t|^2)
|
|
1526
|
+
# => dt/dw_k = +1/(3 sum |t|^2)
|
|
1527
|
+
grad_w_sorted = jnp.where(
|
|
1528
|
+
denom > 0,
|
|
1529
|
+
sumg / (3.0 * denom),
|
|
1530
|
+
jnp.array(0.0, dtype=dtype),
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
return (
|
|
1534
|
+
grad_z_sorted[inv_perm_z].astype(orig_dtype),
|
|
1535
|
+
grad_w_sorted[inv_perm_w].astype(orig_dtype),
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
_proj_permutahedron_pnorm_q4.defvjp(
|
|
1540
|
+
_proj_permutahedron_pnorm_q4_fwd,
|
|
1541
|
+
_proj_permutahedron_pnorm_q4_bwd,
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
# -- entropic isotonic regression (smooth mode) -----------------------
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
def _pav_isotonic_decreasing_entropic(
|
|
1549
|
+
s: jax.Array,
|
|
1550
|
+
w: jax.Array,
|
|
1551
|
+
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array, jax.Array]:
|
|
1552
|
+
"""Entropic isotonic regression via PAV.
|
|
1553
|
+
|
|
1554
|
+
Solve v_E(s,w) = argmin_{v1>=...>=vn}
|
|
1555
|
+
<exp(s - v), 1> + <exp(w), v>.
|
|
1556
|
+
Returns v and block structure.
|
|
1557
|
+
"""
|
|
1558
|
+
s = jnp.asarray(s)
|
|
1559
|
+
w = jnp.asarray(w)
|
|
1560
|
+
n = s.shape[0]
|
|
1561
|
+
dtype = s.dtype
|
|
1562
|
+
neg_inf = jnp.array(-jnp.inf, dtype=dtype)
|
|
1563
|
+
|
|
1564
|
+
starts0 = jnp.full((n,), n, dtype=jnp.int32)
|
|
1565
|
+
# logsumexp over s in block
|
|
1566
|
+
logS0 = jnp.full((n,), neg_inf, dtype=dtype)
|
|
1567
|
+
# logsumexp over w in block
|
|
1568
|
+
logW0 = jnp.full((n,), neg_inf, dtype=dtype)
|
|
1569
|
+
m0 = jnp.int32(0)
|
|
1570
|
+
|
|
1571
|
+
def gamma(
|
|
1572
|
+
logS: jax.Array,
|
|
1573
|
+
logW: jax.Array,
|
|
1574
|
+
) -> jax.Array:
|
|
1575
|
+
"""Compute the entropic block level as logS - logW."""
|
|
1576
|
+
return logS - logW
|
|
1577
|
+
|
|
1578
|
+
def merge_cond(state: tuple) -> jax.Array:
|
|
1579
|
+
"""Check whether the top two entropic blocks violate isotonic order."""
|
|
1580
|
+
starts, logS, logW, m = state
|
|
1581
|
+
return (m >= 2) & (gamma(logS[m - 2], logW[m - 2]) < gamma(logS[m - 1], logW[m - 1]))
|
|
1582
|
+
|
|
1583
|
+
def merge_body(state: tuple) -> tuple:
|
|
1584
|
+
"""Merge the top two entropic blocks on the stack."""
|
|
1585
|
+
starts, logS, logW, m = state
|
|
1586
|
+
i_prev = m - 2
|
|
1587
|
+
i_top = m - 1
|
|
1588
|
+
|
|
1589
|
+
logS = logS.at[i_prev].set(
|
|
1590
|
+
jnp.logaddexp(logS[i_prev], logS[i_top]),
|
|
1591
|
+
)
|
|
1592
|
+
logW = logW.at[i_prev].set(
|
|
1593
|
+
jnp.logaddexp(logW[i_prev], logW[i_top]),
|
|
1594
|
+
)
|
|
1595
|
+
|
|
1596
|
+
logS = logS.at[i_top].set(neg_inf)
|
|
1597
|
+
logW = logW.at[i_top].set(neg_inf)
|
|
1598
|
+
starts = starts.at[i_top].set(jnp.int32(n))
|
|
1599
|
+
|
|
1600
|
+
return (starts, logS, logW, m - 1)
|
|
1601
|
+
|
|
1602
|
+
def for_body(
|
|
1603
|
+
i: jax.Array,
|
|
1604
|
+
state: tuple,
|
|
1605
|
+
) -> tuple:
|
|
1606
|
+
"""Push element i onto the entropic stack and merge violations."""
|
|
1607
|
+
starts, logS, logW, m = state
|
|
1608
|
+
starts = starts.at[m].set(jnp.int32(i))
|
|
1609
|
+
logS = logS.at[m].set(s[i])
|
|
1610
|
+
logW = logW.at[m].set(w[i])
|
|
1611
|
+
m = m + 1
|
|
1612
|
+
return lax.while_loop(
|
|
1613
|
+
merge_cond,
|
|
1614
|
+
merge_body,
|
|
1615
|
+
(starts, logS, logW, m),
|
|
1616
|
+
)
|
|
1617
|
+
|
|
1618
|
+
starts_out, logS_out, logW_out, m = lax.fori_loop(
|
|
1619
|
+
0,
|
|
1620
|
+
n,
|
|
1621
|
+
for_body,
|
|
1622
|
+
(starts0, logS0, logW0, m0),
|
|
1623
|
+
)
|
|
1624
|
+
starts: Array = jnp.asarray(starts_out)
|
|
1625
|
+
logS: Array = jnp.asarray(logS_out)
|
|
1626
|
+
logW: Array = jnp.asarray(logW_out)
|
|
1627
|
+
|
|
1628
|
+
idx = jnp.arange(n, dtype=jnp.int32)
|
|
1629
|
+
starts = jnp.where(idx < m, starts, jnp.int32(n))
|
|
1630
|
+
logS = jnp.where(idx < m, logS, neg_inf)
|
|
1631
|
+
logW = jnp.where(idx < m, logW, neg_inf)
|
|
1632
|
+
|
|
1633
|
+
block_idx: Array = jnp.searchsorted(starts, idx, side="right") - jnp.int32(1)
|
|
1634
|
+
gammas = logS - logW
|
|
1635
|
+
v: Array = gammas[block_idx]
|
|
1636
|
+
return v, block_idx, starts, logS, logW
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
@jax.custom_vjp
|
|
1640
|
+
def _proj_permutahedron_entropic(
|
|
1641
|
+
z: jax.Array,
|
|
1642
|
+
w: jax.Array,
|
|
1643
|
+
) -> jax.Array:
|
|
1644
|
+
"""Project z onto the permutahedron of w using entropic mode."""
|
|
1645
|
+
z = jnp.asarray(z)
|
|
1646
|
+
w = jnp.asarray(w)
|
|
1647
|
+
|
|
1648
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
1649
|
+
z_sorted = z[perm_z]
|
|
1650
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
1651
|
+
|
|
1652
|
+
perm_w = jnp.argsort(-w, stable=True)
|
|
1653
|
+
w_sorted = w[perm_w]
|
|
1654
|
+
|
|
1655
|
+
v, _, _, _, _ = _pav_isotonic_decreasing_entropic(
|
|
1656
|
+
z_sorted,
|
|
1657
|
+
w_sorted,
|
|
1658
|
+
)
|
|
1659
|
+
p_sorted = z_sorted - v
|
|
1660
|
+
return p_sorted[inv_perm_z]
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
def _proj_permutahedron_entropic_fwd(
|
|
1664
|
+
z: jax.Array,
|
|
1665
|
+
w: jax.Array,
|
|
1666
|
+
) -> tuple[jax.Array, tuple]:
|
|
1667
|
+
"""Forward pass for entropic projection custom VJP."""
|
|
1668
|
+
z = jnp.asarray(z)
|
|
1669
|
+
w = jnp.asarray(w)
|
|
1670
|
+
|
|
1671
|
+
perm_z = jnp.argsort(-z, stable=True)
|
|
1672
|
+
z_sorted = z[perm_z]
|
|
1673
|
+
inv_perm_z = _inv_permutation(perm_z)
|
|
1674
|
+
|
|
1675
|
+
perm_w = jnp.argsort(-w, stable=True)
|
|
1676
|
+
w_sorted = w[perm_w]
|
|
1677
|
+
inv_perm_w = _inv_permutation(perm_w)
|
|
1678
|
+
|
|
1679
|
+
v, block_idx, _, logS, logW = _pav_isotonic_decreasing_entropic(z_sorted, w_sorted)
|
|
1680
|
+
p_sorted = z_sorted - v
|
|
1681
|
+
p = p_sorted[inv_perm_z]
|
|
1682
|
+
|
|
1683
|
+
aux = (
|
|
1684
|
+
perm_z,
|
|
1685
|
+
inv_perm_z,
|
|
1686
|
+
perm_w,
|
|
1687
|
+
inv_perm_w,
|
|
1688
|
+
block_idx,
|
|
1689
|
+
z_sorted,
|
|
1690
|
+
w_sorted,
|
|
1691
|
+
logS,
|
|
1692
|
+
logW,
|
|
1693
|
+
)
|
|
1694
|
+
return p, aux
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def _proj_permutahedron_entropic_bwd(
|
|
1698
|
+
aux: tuple,
|
|
1699
|
+
g: jax.Array,
|
|
1700
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
1701
|
+
"""Backward pass for entropic projection custom VJP."""
|
|
1702
|
+
(
|
|
1703
|
+
perm_z,
|
|
1704
|
+
inv_perm_z,
|
|
1705
|
+
perm_w,
|
|
1706
|
+
inv_perm_w,
|
|
1707
|
+
block_idx,
|
|
1708
|
+
z_sorted,
|
|
1709
|
+
w_sorted,
|
|
1710
|
+
logS,
|
|
1711
|
+
logW,
|
|
1712
|
+
) = aux
|
|
1713
|
+
g = jnp.asarray(g)
|
|
1714
|
+
n = g.shape[0]
|
|
1715
|
+
|
|
1716
|
+
g_sorted = g[perm_z]
|
|
1717
|
+
|
|
1718
|
+
# per-block sum of incoming gradients
|
|
1719
|
+
block_sum_g = segment_sum(
|
|
1720
|
+
g_sorted,
|
|
1721
|
+
block_idx,
|
|
1722
|
+
num_segments=n,
|
|
1723
|
+
) # (n,)
|
|
1724
|
+
|
|
1725
|
+
# softmax weights within each block
|
|
1726
|
+
logS_b = logS[block_idx]
|
|
1727
|
+
logW_b = logW[block_idx]
|
|
1728
|
+
p_s = jnp.exp(z_sorted - logS_b)
|
|
1729
|
+
q_w = jnp.exp(w_sorted - logW_b)
|
|
1730
|
+
|
|
1731
|
+
sum_g = block_sum_g[block_idx]
|
|
1732
|
+
Jt_g_s = p_s * sum_g # (d v / d s)^T g
|
|
1733
|
+
Jt_g_w = (-q_w) * sum_g # (d v / d w)^T g
|
|
1734
|
+
|
|
1735
|
+
grad_z_sorted = g_sorted - Jt_g_s
|
|
1736
|
+
# dp/dw = -(d v / d w) => vjp adds minus
|
|
1737
|
+
grad_w_sorted = -Jt_g_w
|
|
1738
|
+
|
|
1739
|
+
return (
|
|
1740
|
+
grad_z_sorted[inv_perm_z],
|
|
1741
|
+
grad_w_sorted[inv_perm_w],
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
_proj_permutahedron_entropic.defvjp(
|
|
1746
|
+
_proj_permutahedron_entropic_fwd,
|
|
1747
|
+
_proj_permutahedron_entropic_bwd,
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
# -- dispatcher --------------------------------------------------------
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
def proj_permutahedron(
|
|
1755
|
+
z: Array, # (..., n)
|
|
1756
|
+
w: Array, # (..., n)
|
|
1757
|
+
softness: float | Array = 0.1,
|
|
1758
|
+
mode: Literal["smooth", "c0", "c1", "c2"] = "smooth",
|
|
1759
|
+
) -> Array: # (..., n)
|
|
1760
|
+
"""Project ``z`` onto the permutahedron of ``w``.
|
|
1761
|
+
|
|
1762
|
+
Solves the optimization problem::
|
|
1763
|
+
|
|
1764
|
+
min_y <z, y> + softness * R(y)
|
|
1765
|
+
s.t. y in Perm(w)
|
|
1766
|
+
|
|
1767
|
+
where R(y) is the regularizer determined by ``mode``.
|
|
1768
|
+
|
|
1769
|
+
Args:
|
|
1770
|
+
z: Input array of shape ``(..., n)`` to be projected.
|
|
1771
|
+
w: Array of shape ``(..., n)`` defining the permutahedron.
|
|
1772
|
+
softness: Controls the strength of the regularizer.
|
|
1773
|
+
Must be positive.
|
|
1774
|
+
mode: Controls the type of regularizer:
|
|
1775
|
+
- ``"smooth"``: Entropic (log-KL) projection onto the
|
|
1776
|
+
permutahedron. Solved via isotonic regression. Not
|
|
1777
|
+
fully C-infinity due to argsort discontinuities at
|
|
1778
|
+
the boundary of sorting chambers.
|
|
1779
|
+
- ``"c0"``: C0 continuous (euclidean/L2 regularizer).
|
|
1780
|
+
Euclidean projection onto the permutahedron.
|
|
1781
|
+
- ``"c1"``: C1 differentiable (p=3/2 p-norm). p-norm
|
|
1782
|
+
projection via PAV with closed-form block solvers.
|
|
1783
|
+
- ``"c2"``: C2 twice differentiable (p=4/3 p-norm).
|
|
1784
|
+
p-norm projection via PAV with closed-form block
|
|
1785
|
+
solvers.
|
|
1786
|
+
|
|
1787
|
+
Returns:
|
|
1788
|
+
Array of shape ``(..., n)`` representing the projected
|
|
1789
|
+
values onto the permutahedron of ``w``.
|
|
1790
|
+
"""
|
|
1791
|
+
if z.shape != w.shape:
|
|
1792
|
+
msg = f"Shapes of z and w must match, but got z.shape={z.shape} and w.shape={w.shape}."
|
|
1793
|
+
raise ValueError(msg)
|
|
1794
|
+
validate_softness(softness)
|
|
1795
|
+
*batch_sizes, n = z.shape
|
|
1796
|
+
z_batched = z.reshape(-1, n) # (B, n)
|
|
1797
|
+
w_batched = w.reshape(-1, n) # (B, n)
|
|
1798
|
+
z_batched = z_batched / softness
|
|
1799
|
+
if mode == "smooth":
|
|
1800
|
+
proj_fn = _proj_permutahedron_entropic
|
|
1801
|
+
elif mode == "c0":
|
|
1802
|
+
# Curvature of (1/2)||y||^2: R''=1, no scaling needed
|
|
1803
|
+
proj_fn = _proj_permutahedron_pnorm_q2
|
|
1804
|
+
elif mode == "c1":
|
|
1805
|
+
proj_fn = _proj_permutahedron_pnorm_q3
|
|
1806
|
+
elif mode == "c2":
|
|
1807
|
+
proj_fn = _proj_permutahedron_pnorm_q4
|
|
1808
|
+
else:
|
|
1809
|
+
msg = f"Invalid mode: {mode}"
|
|
1810
|
+
raise ValueError(msg)
|
|
1811
|
+
soft_values = jax.vmap(proj_fn, in_axes=(0, 0))(
|
|
1812
|
+
z_batched,
|
|
1813
|
+
w_batched,
|
|
1814
|
+
) # (B, n)
|
|
1815
|
+
soft_values = soft_values.reshape(*batch_sizes, n)
|
|
1816
|
+
return soft_values
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
def proj_permutahedron_smooth_sort(
|
|
1820
|
+
z: Array, # (..., n)
|
|
1821
|
+
w: Array, # (..., n)
|
|
1822
|
+
softness: float | Array = 0.1,
|
|
1823
|
+
lbfgs_tol: float = 1e-5,
|
|
1824
|
+
lbfgs_max_iter: int = 10000,
|
|
1825
|
+
) -> Array: # (..., n)
|
|
1826
|
+
"""Project ``z`` onto the permutahedron of ``w`` via C-inf ESP+LBFGS.
|
|
1827
|
+
|
|
1828
|
+
Uses smooth majorization bounds (elementary symmetric polynomials)
|
|
1829
|
+
for C-infinity gradients w.r.t. ``w``, and an LBFGS dual solver
|
|
1830
|
+
for the entropic LP relaxation.
|
|
1831
|
+
|
|
1832
|
+
Requires the ``optimistix`` package.
|
|
1833
|
+
|
|
1834
|
+
Args:
|
|
1835
|
+
z: Input array of shape ``(..., n)`` to be projected.
|
|
1836
|
+
w: Array of shape ``(..., n)`` defining the permutahedron.
|
|
1837
|
+
softness: Controls the strength of the regularizer.
|
|
1838
|
+
Must be positive.
|
|
1839
|
+
lbfgs_tol: Tolerance for the LBFGS solver.
|
|
1840
|
+
lbfgs_max_iter: Maximum iterations for the LBFGS solver.
|
|
1841
|
+
|
|
1842
|
+
Returns:
|
|
1843
|
+
Array of shape ``(..., n)`` representing the projected
|
|
1844
|
+
values onto the permutahedron of ``w``.
|
|
1845
|
+
"""
|
|
1846
|
+
if z.shape != w.shape:
|
|
1847
|
+
msg = f"Shapes of z and w must match, but got z.shape={z.shape} and w.shape={w.shape}."
|
|
1848
|
+
raise ValueError(msg)
|
|
1849
|
+
validate_softness(softness)
|
|
1850
|
+
*batch_sizes, n = z.shape
|
|
1851
|
+
z_batched = z.reshape(-1, n) # (B, n)
|
|
1852
|
+
w_batched = w.reshape(-1, n) # (B, n)
|
|
1853
|
+
z_batched = z_batched / softness
|
|
1854
|
+
proj_fn = _make_proj_permutahedron_entropic_lp(
|
|
1855
|
+
tol=lbfgs_tol,
|
|
1856
|
+
max_iter=lbfgs_max_iter,
|
|
1857
|
+
bounds_softness=jnp.minimum(softness, 1.0),
|
|
1858
|
+
)
|
|
1859
|
+
soft_values = jax.vmap(proj_fn, in_axes=(0, 0))(
|
|
1860
|
+
z_batched,
|
|
1861
|
+
w_batched,
|
|
1862
|
+
) # (B, n)
|
|
1863
|
+
soft_values = soft_values.reshape(*batch_sizes, n)
|
|
1864
|
+
return soft_values
|