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