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,261 @@
|
|
|
1
|
+
"""Straight-through estimator decorators and _st variants.
|
|
2
|
+
|
|
3
|
+
Straight-through estimators use the hard (exact, non-differentiable)
|
|
4
|
+
function for the forward pass but route gradients through the soft
|
|
5
|
+
(differentiable) version during backpropagation.
|
|
6
|
+
|
|
7
|
+
This module provides:
|
|
8
|
+
- :func:`st`: Decorator that creates a straight-through version of any
|
|
9
|
+
soft_ops function with a ``mode`` parameter.
|
|
10
|
+
- :func:`grad_replace`: Lower-level decorator for custom forward/backward
|
|
11
|
+
split functions.
|
|
12
|
+
- 27 pre-built ``_st`` variants (e.g., :func:`relu_st`, :func:`sort_st`).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import functools
|
|
16
|
+
import inspect
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
|
|
19
|
+
import jax
|
|
20
|
+
from jax import tree_util as jtu
|
|
21
|
+
|
|
22
|
+
from diffbio.core.soft_ops import comparison, elementwise, quantile, sorting
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def grad_replace(fn: Callable) -> Callable:
|
|
26
|
+
"""Decorator for custom forward/backward computation split.
|
|
27
|
+
|
|
28
|
+
The decorated function is called twice: once with ``forward=True``
|
|
29
|
+
(output used for forward pass) and once with ``forward=False``
|
|
30
|
+
(output used for gradient computation).
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
fn: Function accepting a ``forward: bool`` keyword argument.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Wrapped function using hard forward, soft backward.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@functools.wraps(fn)
|
|
40
|
+
def wrapped(*args, **kwargs):
|
|
41
|
+
fw_y = fn(*args, **kwargs, forward=True)
|
|
42
|
+
bw_y = fn(*args, **kwargs, forward=False)
|
|
43
|
+
fw_leaves, fw_treedef = jtu.tree_flatten(
|
|
44
|
+
fw_y,
|
|
45
|
+
is_leaf=lambda x: x is None,
|
|
46
|
+
)
|
|
47
|
+
bw_leaves, _ = jtu.tree_flatten(
|
|
48
|
+
bw_y,
|
|
49
|
+
is_leaf=lambda x: x is None,
|
|
50
|
+
)
|
|
51
|
+
out_leaves = [
|
|
52
|
+
f if f is None or b is None else jax.lax.stop_gradient(f - b) + b
|
|
53
|
+
for f, b in zip(fw_leaves, bw_leaves)
|
|
54
|
+
]
|
|
55
|
+
return jtu.tree_unflatten(fw_treedef, out_leaves)
|
|
56
|
+
|
|
57
|
+
return wrapped
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def st(fn: Callable) -> Callable:
|
|
61
|
+
"""Decorator creating a straight-through estimator from a soft_ops function.
|
|
62
|
+
|
|
63
|
+
The decorated function is called twice: once with ``mode="hard"``
|
|
64
|
+
(forward pass) and once with the specified ``mode`` (backward pass).
|
|
65
|
+
The trick ``stop_gradient(hard - soft) + soft`` ensures the forward
|
|
66
|
+
output is hard but gradients flow through the soft version.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
fn: Function with a ``mode`` parameter (e.g., any elementwise,
|
|
70
|
+
comparison, or sorting function).
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Wrapped straight-through estimator function.
|
|
74
|
+
"""
|
|
75
|
+
sig = inspect.signature(fn)
|
|
76
|
+
mode_param = sig.parameters.get("mode")
|
|
77
|
+
if mode_param is not None:
|
|
78
|
+
mode_default = mode_param.default
|
|
79
|
+
mode_idx = list(sig.parameters.keys()).index("mode")
|
|
80
|
+
else:
|
|
81
|
+
mode_default = "smooth"
|
|
82
|
+
mode_idx = None
|
|
83
|
+
|
|
84
|
+
@functools.wraps(fn)
|
|
85
|
+
def wrapped(*args, **kwargs):
|
|
86
|
+
if mode_idx is not None and len(args) > mode_idx:
|
|
87
|
+
mode = args[mode_idx]
|
|
88
|
+
args = args[:mode_idx] + args[mode_idx + 1 :]
|
|
89
|
+
else:
|
|
90
|
+
mode = kwargs.pop("mode", mode_default)
|
|
91
|
+
fw_y = fn(*args, **kwargs, mode="hard")
|
|
92
|
+
bw_y = fn(*args, **kwargs, mode=mode)
|
|
93
|
+
fw_leaves, fw_treedef = jtu.tree_flatten(
|
|
94
|
+
fw_y,
|
|
95
|
+
is_leaf=lambda x: x is None,
|
|
96
|
+
)
|
|
97
|
+
bw_leaves, _ = jtu.tree_flatten(
|
|
98
|
+
bw_y,
|
|
99
|
+
is_leaf=lambda x: x is None,
|
|
100
|
+
)
|
|
101
|
+
out_leaves = [
|
|
102
|
+
f if f is None or b is None else jax.lax.stop_gradient(f - b) + b
|
|
103
|
+
for f, b in zip(fw_leaves, bw_leaves)
|
|
104
|
+
]
|
|
105
|
+
return jtu.tree_unflatten(fw_treedef, out_leaves)
|
|
106
|
+
|
|
107
|
+
return wrapped
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# Cached ST wrapper to avoid repeated inspect.signature() calls
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
_st_cache: dict[Callable, Callable] = {}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _cached_st(fn: Callable) -> Callable:
|
|
118
|
+
"""Return a cached st() wrapper."""
|
|
119
|
+
if fn not in _st_cache:
|
|
120
|
+
_st_cache[fn] = st(fn)
|
|
121
|
+
return _st_cache[fn]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# 27 pre-built straight-through variants
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def abs_st(*args, **kwargs):
|
|
130
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.elementwise.abs`."""
|
|
131
|
+
return _cached_st(elementwise.abs)(*args, **kwargs)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def argmax_st(*args, **kwargs):
|
|
135
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.sorting.argmax`."""
|
|
136
|
+
return _cached_st(sorting.argmax)(*args, **kwargs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def argmedian_st(*args, **kwargs):
|
|
140
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.quantile.argmedian`."""
|
|
141
|
+
return _cached_st(quantile.argmedian)(*args, **kwargs)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def argmin_st(*args, **kwargs):
|
|
145
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.sorting.argmin`."""
|
|
146
|
+
return _cached_st(sorting.argmin)(*args, **kwargs)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def argpercentile_st(*args, **kwargs):
|
|
150
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.quantile.argpercentile`."""
|
|
151
|
+
return _cached_st(quantile.argpercentile)(*args, **kwargs)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def argquantile_st(*args, **kwargs):
|
|
155
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.quantile.argquantile`."""
|
|
156
|
+
return _cached_st(quantile.argquantile)(*args, **kwargs)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def argsort_st(*args, **kwargs):
|
|
160
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.sorting.argsort`."""
|
|
161
|
+
return _cached_st(sorting.argsort)(*args, **kwargs)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def clip_st(*args, **kwargs):
|
|
165
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.elementwise.clip`."""
|
|
166
|
+
return _cached_st(elementwise.clip)(*args, **kwargs)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def equal_st(*args, **kwargs):
|
|
170
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.comparison.equal`."""
|
|
171
|
+
return _cached_st(comparison.equal)(*args, **kwargs)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def greater_equal_st(*args, **kwargs):
|
|
175
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.comparison.greater_equal`."""
|
|
176
|
+
return _cached_st(comparison.greater_equal)(*args, **kwargs)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def greater_st(*args, **kwargs):
|
|
180
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.comparison.greater`."""
|
|
181
|
+
return _cached_st(comparison.greater)(*args, **kwargs)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def heaviside_st(*args, **kwargs):
|
|
185
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.elementwise.heaviside`."""
|
|
186
|
+
return _cached_st(elementwise.heaviside)(*args, **kwargs)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def isclose_st(*args, **kwargs):
|
|
190
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.comparison.isclose`."""
|
|
191
|
+
return _cached_st(comparison.isclose)(*args, **kwargs)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def less_equal_st(*args, **kwargs):
|
|
195
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.comparison.less_equal`."""
|
|
196
|
+
return _cached_st(comparison.less_equal)(*args, **kwargs)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def less_st(*args, **kwargs):
|
|
200
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.comparison.less`."""
|
|
201
|
+
return _cached_st(comparison.less)(*args, **kwargs)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def max_st(*args, **kwargs):
|
|
205
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.sorting.max`."""
|
|
206
|
+
return _cached_st(sorting.max)(*args, **kwargs)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def median_st(*args, **kwargs):
|
|
210
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.quantile.median`."""
|
|
211
|
+
return _cached_st(quantile.median)(*args, **kwargs)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def min_st(*args, **kwargs):
|
|
215
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.sorting.min`."""
|
|
216
|
+
return _cached_st(sorting.min)(*args, **kwargs)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def not_equal_st(*args, **kwargs):
|
|
220
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.comparison.not_equal`."""
|
|
221
|
+
return _cached_st(comparison.not_equal)(*args, **kwargs)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def percentile_st(*args, **kwargs):
|
|
225
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.quantile.percentile`."""
|
|
226
|
+
return _cached_st(quantile.percentile)(*args, **kwargs)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def quantile_st(*args, **kwargs):
|
|
230
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.quantile.quantile`."""
|
|
231
|
+
return _cached_st(quantile.quantile)(*args, **kwargs)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def rank_st(*args, **kwargs):
|
|
235
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.sorting.rank`."""
|
|
236
|
+
return _cached_st(sorting.rank)(*args, **kwargs)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def relu_st(*args, **kwargs):
|
|
240
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.elementwise.relu`."""
|
|
241
|
+
return _cached_st(elementwise.relu)(*args, **kwargs)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def round_st(*args, **kwargs):
|
|
245
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.elementwise.round`."""
|
|
246
|
+
return _cached_st(elementwise.round)(*args, **kwargs)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def sign_st(*args, **kwargs):
|
|
250
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.elementwise.sign`."""
|
|
251
|
+
return _cached_st(elementwise.sign)(*args, **kwargs)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def sort_st(*args, **kwargs):
|
|
255
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.sorting.sort`."""
|
|
256
|
+
return _cached_st(sorting.sort)(*args, **kwargs)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def top_k_st(*args, **kwargs):
|
|
260
|
+
"""Straight-through :func:`~diffbio.core.soft_ops.sorting.top_k`."""
|
|
261
|
+
return _cached_st(sorting.top_k)(*args, **kwargs)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Uncertainty quantification wrappers for DiffBio operators.
|
|
2
|
+
|
|
3
|
+
Provides ensemble and conformal prediction wrappers that add
|
|
4
|
+
``uncertainty``, ``confidence_interval_lower``, and
|
|
5
|
+
``confidence_interval_upper`` keys to any operator's output dict.
|
|
6
|
+
|
|
7
|
+
Uses ``nnx.vmap`` with ``nnx.StateAxes`` to vectorize multiple forward
|
|
8
|
+
passes across different RNG seeds (ensemble) or dropout samples
|
|
9
|
+
(conformal), computing mean predictions and uncertainty estimates
|
|
10
|
+
without Python for-loops.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import jax
|
|
20
|
+
import jax.numpy as jnp
|
|
21
|
+
from datarax.core.config import OperatorConfig
|
|
22
|
+
from datarax.core.operator import OperatorModule
|
|
23
|
+
from flax import nnx
|
|
24
|
+
from jaxtyping import PyTree
|
|
25
|
+
|
|
26
|
+
from diffbio.core import soft_ops
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class EnsembleUQConfig(OperatorConfig):
|
|
33
|
+
"""Configuration for ensemble-based uncertainty quantification.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
n_members: Number of ensemble members (forward passes with
|
|
37
|
+
different random seeds).
|
|
38
|
+
confidence_level: Confidence level for intervals (0 to 1).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
n_members: int = 5
|
|
42
|
+
confidence_level: float = 0.95
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ConformalUQConfig(OperatorConfig):
|
|
47
|
+
"""Configuration for conformal prediction-based UQ.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
alpha: Significance level (1 - confidence). Smaller alpha
|
|
51
|
+
gives wider intervals.
|
|
52
|
+
num_samples: Number of Monte Carlo samples for interval estimation.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
alpha: float = 0.1
|
|
56
|
+
num_samples: int = 20
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _find_primary_output_key(result: dict[str, Any], data: dict[str, Any]) -> str | None:
|
|
60
|
+
"""Find the main output key added by the operator.
|
|
61
|
+
|
|
62
|
+
Returns the first key in result that is not in data and holds a JAX array.
|
|
63
|
+
"""
|
|
64
|
+
for key in result:
|
|
65
|
+
if key not in data and isinstance(result[key], jax.Array):
|
|
66
|
+
return key
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _run_n_samples(
|
|
71
|
+
operator: OperatorModule,
|
|
72
|
+
data: dict[str, Any],
|
|
73
|
+
state: dict[str, Any],
|
|
74
|
+
metadata: dict[str, Any] | None,
|
|
75
|
+
n_samples: int,
|
|
76
|
+
primary_key: str,
|
|
77
|
+
) -> jax.Array:
|
|
78
|
+
"""Run operator n_samples times and stack the primary output.
|
|
79
|
+
|
|
80
|
+
Uses jax.lax.scan to avoid Python for-loops. The operator is
|
|
81
|
+
deterministic per call (same params, same data), so all samples
|
|
82
|
+
are identical for non-stochastic operators. For stochastic operators
|
|
83
|
+
(dropout, noise), each call samples fresh randomness via NNX RNG state.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
operator: The base operator to run.
|
|
87
|
+
data: Input data dict.
|
|
88
|
+
state: Element state.
|
|
89
|
+
metadata: Element metadata.
|
|
90
|
+
n_samples: Number of forward passes.
|
|
91
|
+
primary_key: Key to stack from the output dict.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Stacked array of shape (n_samples, *output_shape).
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def _step(_carry: None, _xs: None) -> tuple[None, jax.Array]:
|
|
98
|
+
result, _, _ = operator.apply(data, state, metadata)
|
|
99
|
+
return None, result[primary_key]
|
|
100
|
+
|
|
101
|
+
_, stacked = jax.lax.scan(_step, None, None, length=n_samples)
|
|
102
|
+
return stacked
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _compute_uq_stats(
|
|
106
|
+
stacked: jax.Array,
|
|
107
|
+
confidence_level: float,
|
|
108
|
+
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
|
|
109
|
+
"""Compute mean, std, and confidence interval from stacked samples.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
stacked: Array of shape (n_samples, *output_shape).
|
|
113
|
+
confidence_level: Confidence level for quantile intervals.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Tuple of (mean, std, lower, upper).
|
|
117
|
+
"""
|
|
118
|
+
mean = jnp.mean(stacked, axis=0)
|
|
119
|
+
std = jnp.std(stacked, axis=0)
|
|
120
|
+
alpha = 1.0 - confidence_level
|
|
121
|
+
lower = soft_ops.quantile(stacked, jnp.array(alpha / 2), axis=0, softness=0.1)
|
|
122
|
+
upper = soft_ops.quantile(stacked, jnp.array(1 - alpha / 2), axis=0, softness=0.1)
|
|
123
|
+
return mean, std, lower, upper
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class EnsembleUQOperator(OperatorModule):
|
|
127
|
+
"""Ensemble-based uncertainty quantification wrapper.
|
|
128
|
+
|
|
129
|
+
Runs the base operator multiple times and aggregates outputs to produce
|
|
130
|
+
mean predictions with uncertainty estimates (standard deviation) and
|
|
131
|
+
confidence intervals (quantile-based). Uses ``jax.lax.scan`` to avoid
|
|
132
|
+
Python for-loops.
|
|
133
|
+
|
|
134
|
+
Output adds:
|
|
135
|
+
- ``uncertainty``: Standard deviation across ensemble members.
|
|
136
|
+
- ``confidence_interval_lower``: Lower bound of confidence interval.
|
|
137
|
+
- ``confidence_interval_upper``: Upper bound of confidence interval.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
config: EnsembleUQConfig with ensemble parameters.
|
|
141
|
+
base_operator: The operator to wrap.
|
|
142
|
+
rngs: Random number generators.
|
|
143
|
+
name: Optional operator name.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
config: EnsembleUQConfig,
|
|
149
|
+
*,
|
|
150
|
+
base_operator: OperatorModule,
|
|
151
|
+
rngs: nnx.Rngs | None = None,
|
|
152
|
+
name: str | None = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Initialize ensemble UQ operator."""
|
|
155
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
156
|
+
self.config: EnsembleUQConfig = config
|
|
157
|
+
self.base_operator = base_operator
|
|
158
|
+
|
|
159
|
+
def apply(
|
|
160
|
+
self,
|
|
161
|
+
data: PyTree,
|
|
162
|
+
state: PyTree,
|
|
163
|
+
metadata: dict[str, Any] | None,
|
|
164
|
+
random_params: Any = None, # noqa: ARG002
|
|
165
|
+
stats: dict[str, Any] | None = None, # noqa: ARG002
|
|
166
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
167
|
+
"""Run ensemble forward passes and aggregate with uncertainty.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
data: Input data dict for the base operator.
|
|
171
|
+
state: Element state (passed through).
|
|
172
|
+
metadata: Element metadata (passed through).
|
|
173
|
+
random_params: Unused.
|
|
174
|
+
stats: Unused.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Tuple of (output_with_uncertainty, state, metadata).
|
|
178
|
+
"""
|
|
179
|
+
# Get a single result to find output keys and structure
|
|
180
|
+
base_result, _, _ = self.base_operator.apply(data, state, metadata)
|
|
181
|
+
primary_key = _find_primary_output_key(base_result, data)
|
|
182
|
+
if primary_key is None:
|
|
183
|
+
return base_result, state, metadata
|
|
184
|
+
|
|
185
|
+
stacked = _run_n_samples(
|
|
186
|
+
self.base_operator,
|
|
187
|
+
data,
|
|
188
|
+
state,
|
|
189
|
+
metadata,
|
|
190
|
+
self.config.n_members,
|
|
191
|
+
primary_key,
|
|
192
|
+
)
|
|
193
|
+
mean, std, lower, upper = _compute_uq_stats(
|
|
194
|
+
stacked,
|
|
195
|
+
self.config.confidence_level,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
result = dict(base_result)
|
|
199
|
+
result[primary_key] = mean
|
|
200
|
+
result["uncertainty"] = std
|
|
201
|
+
result["confidence_interval_lower"] = lower
|
|
202
|
+
result["confidence_interval_upper"] = upper
|
|
203
|
+
return result, state, metadata
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ConformalUQOperator(OperatorModule):
|
|
207
|
+
"""Conformal prediction-based uncertainty quantification wrapper.
|
|
208
|
+
|
|
209
|
+
Uses Monte Carlo sampling to estimate empirical prediction intervals.
|
|
210
|
+
Runs the base operator multiple times via ``jax.lax.scan`` and computes
|
|
211
|
+
quantile-based intervals at the specified confidence level (1 - alpha).
|
|
212
|
+
|
|
213
|
+
Output adds:
|
|
214
|
+
- ``uncertainty``: Standard deviation across samples.
|
|
215
|
+
- ``confidence_interval_lower``: Lower quantile bound.
|
|
216
|
+
- ``confidence_interval_upper``: Upper quantile bound.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
config: ConformalUQConfig with sampling parameters.
|
|
220
|
+
base_operator: The operator to wrap.
|
|
221
|
+
rngs: Random number generators.
|
|
222
|
+
name: Optional operator name.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(
|
|
226
|
+
self,
|
|
227
|
+
config: ConformalUQConfig,
|
|
228
|
+
*,
|
|
229
|
+
base_operator: OperatorModule,
|
|
230
|
+
rngs: nnx.Rngs | None = None,
|
|
231
|
+
name: str | None = None,
|
|
232
|
+
) -> None:
|
|
233
|
+
"""Initialize conformal UQ operator."""
|
|
234
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
235
|
+
self.config: ConformalUQConfig = config
|
|
236
|
+
self.base_operator = base_operator
|
|
237
|
+
|
|
238
|
+
def apply(
|
|
239
|
+
self,
|
|
240
|
+
data: PyTree,
|
|
241
|
+
state: PyTree,
|
|
242
|
+
metadata: dict[str, Any] | None,
|
|
243
|
+
random_params: Any = None, # noqa: ARG002
|
|
244
|
+
stats: dict[str, Any] | None = None, # noqa: ARG002
|
|
245
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
246
|
+
"""Run conformal prediction sampling.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
data: Input data dict for the base operator.
|
|
250
|
+
state: Element state (passed through).
|
|
251
|
+
metadata: Element metadata (passed through).
|
|
252
|
+
random_params: Unused.
|
|
253
|
+
stats: Unused.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Tuple of (output_with_intervals, state, metadata).
|
|
257
|
+
"""
|
|
258
|
+
base_result, _, _ = self.base_operator.apply(data, state, metadata)
|
|
259
|
+
primary_key = _find_primary_output_key(base_result, data)
|
|
260
|
+
if primary_key is None:
|
|
261
|
+
return base_result, state, metadata
|
|
262
|
+
|
|
263
|
+
stacked = _run_n_samples(
|
|
264
|
+
self.base_operator,
|
|
265
|
+
data,
|
|
266
|
+
state,
|
|
267
|
+
metadata,
|
|
268
|
+
self.config.num_samples,
|
|
269
|
+
primary_key,
|
|
270
|
+
)
|
|
271
|
+
confidence_level = 1.0 - self.config.alpha
|
|
272
|
+
mean, std, lower, upper = _compute_uq_stats(stacked, confidence_level)
|
|
273
|
+
|
|
274
|
+
result = dict(base_result)
|
|
275
|
+
result[primary_key] = mean
|
|
276
|
+
result["uncertainty"] = std
|
|
277
|
+
result["confidence_interval_lower"] = lower
|
|
278
|
+
result["confidence_interval_upper"] = upper
|
|
279
|
+
return result, state, metadata
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Evaluation harness for scBench and SpatialBench benchmarks.
|
|
2
|
+
|
|
3
|
+
Provides grading algorithms, benchmark problem definitions, task adapters,
|
|
4
|
+
and a runner for evaluating DiffBio operators against real-world benchmark
|
|
5
|
+
problems from single-cell and spatial transcriptomics domains.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from diffbio.evaluation.adapters import TaskAdapter, compute_quality_metrics
|
|
9
|
+
from diffbio.evaluation.graders import (
|
|
10
|
+
GradeResult,
|
|
11
|
+
grade_distribution_comparison,
|
|
12
|
+
grade_label_set_jaccard,
|
|
13
|
+
grade_marker_gene_precision_recall,
|
|
14
|
+
grade_multiple_choice,
|
|
15
|
+
grade_numeric_tolerance,
|
|
16
|
+
)
|
|
17
|
+
from diffbio.evaluation.problem import BenchmarkProblem, load_problems
|
|
18
|
+
from diffbio.evaluation.runner import (
|
|
19
|
+
BenchmarkSummary,
|
|
20
|
+
EvalResult,
|
|
21
|
+
run_benchmark,
|
|
22
|
+
run_problem,
|
|
23
|
+
summarize,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BenchmarkProblem",
|
|
28
|
+
"BenchmarkSummary",
|
|
29
|
+
"EvalResult",
|
|
30
|
+
"GradeResult",
|
|
31
|
+
"TaskAdapter",
|
|
32
|
+
"compute_quality_metrics",
|
|
33
|
+
"grade_distribution_comparison",
|
|
34
|
+
"grade_label_set_jaccard",
|
|
35
|
+
"grade_marker_gene_precision_recall",
|
|
36
|
+
"grade_multiple_choice",
|
|
37
|
+
"grade_numeric_tolerance",
|
|
38
|
+
"load_problems",
|
|
39
|
+
"run_benchmark",
|
|
40
|
+
"run_problem",
|
|
41
|
+
"summarize",
|
|
42
|
+
]
|