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,149 @@
|
|
|
1
|
+
"""Graph utility functions for k-NN graph construction and similarity computation.
|
|
2
|
+
|
|
3
|
+
Provides reusable, differentiable functions for pairwise distance computation,
|
|
4
|
+
k-nearest-neighbor graph construction, fuzzy set membership, and graph
|
|
5
|
+
symmetrization. These primitives underpin UMAP, trajectory inference,
|
|
6
|
+
imputation, and other graph-based operators.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import jax
|
|
10
|
+
import jax.numpy as jnp
|
|
11
|
+
|
|
12
|
+
from diffbio.core import soft_ops
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"compute_pairwise_distances",
|
|
16
|
+
"compute_knn_graph",
|
|
17
|
+
"compute_fuzzy_membership",
|
|
18
|
+
"symmetrize_graph",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def compute_pairwise_distances(
|
|
23
|
+
features: jax.Array,
|
|
24
|
+
metric: str = "euclidean",
|
|
25
|
+
) -> jax.Array:
|
|
26
|
+
"""Compute pairwise distance matrix between all samples.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
features: Input feature matrix of shape ``(n_samples, n_features)``.
|
|
30
|
+
metric: Distance metric, either ``"euclidean"`` or ``"cosine"``.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Distance matrix of shape ``(n_samples, n_samples)`` where entry
|
|
34
|
+
``(i, j)`` is the distance from sample *i* to sample *j*.
|
|
35
|
+
"""
|
|
36
|
+
if metric == "cosine":
|
|
37
|
+
norms = jnp.linalg.norm(features, axis=-1, keepdims=True)
|
|
38
|
+
features_norm = features / jnp.maximum(norms, jnp.finfo(features.dtype).eps)
|
|
39
|
+
similarity = jnp.dot(features_norm, features_norm.T)
|
|
40
|
+
distances = jnp.clip(1.0 - similarity, 0.0, 2.0)
|
|
41
|
+
else:
|
|
42
|
+
diff = features[:, None, :] - features[None, :, :]
|
|
43
|
+
distances = jnp.sqrt(jnp.sum(diff**2, axis=-1) + 1e-16)
|
|
44
|
+
|
|
45
|
+
# Self-distance is zero by definition; float32 matmul cannot guarantee this
|
|
46
|
+
n = features.shape[0]
|
|
47
|
+
distances = distances.at[jnp.diag_indices(n)].set(0.0)
|
|
48
|
+
|
|
49
|
+
return distances
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def compute_knn_graph(
|
|
53
|
+
distances: jax.Array,
|
|
54
|
+
k: int,
|
|
55
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
56
|
+
"""Build a k-nearest-neighbor graph from a dense distance matrix.
|
|
57
|
+
|
|
58
|
+
For each node the *k* closest neighbours (by distance) are selected.
|
|
59
|
+
Self-connections are assumed to already be masked out by setting the
|
|
60
|
+
diagonal to a large value before calling this function.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
distances: Dense distance matrix of shape ``(n, n)``. The diagonal
|
|
64
|
+
should contain large sentinel values (e.g. ``DISTANCE_MASK_SENTINEL``) so that
|
|
65
|
+
self-loops are never selected.
|
|
66
|
+
k: Number of nearest neighbours per node. Clipped to ``n - 1``
|
|
67
|
+
when larger than the number of samples minus one.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A tuple ``(edge_indices, edge_weights)`` where:
|
|
71
|
+
|
|
72
|
+
- ``edge_indices`` has shape ``(n * k_eff, 2)`` with each row
|
|
73
|
+
``[source, target]``.
|
|
74
|
+
- ``edge_weights`` has shape ``(n * k_eff,)`` containing the
|
|
75
|
+
corresponding distances.
|
|
76
|
+
|
|
77
|
+
``k_eff = min(k, n - 1)``.
|
|
78
|
+
"""
|
|
79
|
+
n = distances.shape[0]
|
|
80
|
+
k_eff = min(k, n - 1)
|
|
81
|
+
|
|
82
|
+
# Argsort each row; first k_eff entries are the nearest neighbours
|
|
83
|
+
sorted_indices = jnp.argsort(distances, axis=-1)
|
|
84
|
+
knn_indices = sorted_indices[:, :k_eff] # (n, k_eff)
|
|
85
|
+
|
|
86
|
+
# Source indices: each node repeated k_eff times
|
|
87
|
+
sources = jnp.repeat(jnp.arange(n), k_eff) # (n * k_eff,)
|
|
88
|
+
targets = knn_indices.reshape(-1) # (n * k_eff,)
|
|
89
|
+
|
|
90
|
+
edge_indices = jnp.stack([sources, targets], axis=-1) # (n * k_eff, 2)
|
|
91
|
+
edge_weights = distances[sources, targets] # (n * k_eff,)
|
|
92
|
+
|
|
93
|
+
return edge_indices, edge_weights
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def compute_fuzzy_membership(
|
|
97
|
+
distances: jax.Array,
|
|
98
|
+
k: int,
|
|
99
|
+
softness: float = 0.1,
|
|
100
|
+
) -> jax.Array:
|
|
101
|
+
"""Compute fuzzy set membership using a Gaussian kernel with local bandwidth.
|
|
102
|
+
|
|
103
|
+
The bandwidth (sigma) for each sample is set to the distance to its *k*-th
|
|
104
|
+
nearest neighbour, making the kernel adapt to local density. The diagonal
|
|
105
|
+
of the output is forced to zero (no self-similarity).
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
distances: Dense distance matrix of shape ``(n, n)``. The diagonal
|
|
109
|
+
should contain large sentinel values so that self-distances are
|
|
110
|
+
excluded from the bandwidth computation.
|
|
111
|
+
k: Number of neighbours used to determine the local bandwidth.
|
|
112
|
+
Clipped to ``n - 1`` when larger.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Fuzzy membership matrix of shape ``(n, n)`` with values in ``[0, 1]``.
|
|
116
|
+
"""
|
|
117
|
+
n = distances.shape[0]
|
|
118
|
+
k_eff = min(k, n - 1)
|
|
119
|
+
|
|
120
|
+
# Local bandwidth: distance to the k-th nearest neighbour
|
|
121
|
+
sorted_dists = soft_ops.sort(distances, axis=-1, softness=softness)
|
|
122
|
+
sigma = sorted_dists[:, k_eff - 1 : k_eff].squeeze(-1)
|
|
123
|
+
sigma = jnp.maximum(sigma, 1e-8)
|
|
124
|
+
|
|
125
|
+
# Gaussian kernel with local bandwidth
|
|
126
|
+
p_ij = jnp.exp(-distances / sigma[:, None])
|
|
127
|
+
|
|
128
|
+
# Zero out self-similarity
|
|
129
|
+
p_ij = p_ij * (1.0 - jnp.eye(n))
|
|
130
|
+
|
|
131
|
+
return p_ij
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def symmetrize_graph(adjacency: jax.Array) -> jax.Array:
|
|
135
|
+
"""Symmetrize a directed adjacency matrix via fuzzy set union.
|
|
136
|
+
|
|
137
|
+
Applies the probabilistic (fuzzy) union:
|
|
138
|
+
``p_sym = p + p^T - p * p^T``
|
|
139
|
+
|
|
140
|
+
This ensures the output is symmetric and, when inputs are in ``[0, 1]``,
|
|
141
|
+
the outputs remain in ``[0, 1]``.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
adjacency: Directed adjacency / membership matrix of shape ``(n, n)``.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Symmetric adjacency matrix of shape ``(n, n)``.
|
|
148
|
+
"""
|
|
149
|
+
return adjacency + adjacency.T - adjacency * adjacency.T
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Reusable neural network components for DiffBio.
|
|
2
|
+
|
|
3
|
+
This module provides neural network building blocks that are specific to
|
|
4
|
+
bioinformatics applications and not available in Flax NNX built-ins.
|
|
5
|
+
|
|
6
|
+
IMPORTANT: For standard components, use Flax NNX built-ins:
|
|
7
|
+
- nnx.MultiHeadAttention for attention
|
|
8
|
+
- nnx.Linear for dense layers
|
|
9
|
+
- nnx.Conv for convolutions
|
|
10
|
+
- nnx.LayerNorm for normalization
|
|
11
|
+
- nnx.Dropout for dropout
|
|
12
|
+
- nnx.Sequential for layer composition
|
|
13
|
+
|
|
14
|
+
For reusable components, import from artifex:
|
|
15
|
+
- PositionalEncoding (sinusoidal, learned, RoPE)
|
|
16
|
+
- ResidualBlock1D, ResidualBlock2D
|
|
17
|
+
- TransformerBlock
|
|
18
|
+
- Various loss functions
|
|
19
|
+
|
|
20
|
+
This module only provides DiffBio-specific components:
|
|
21
|
+
- GumbelSoftmaxModule: Differentiable discrete sampling
|
|
22
|
+
- GraphMessagePassing: GNN message passing for graph-structured data
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Literal
|
|
26
|
+
|
|
27
|
+
import jax
|
|
28
|
+
import jax.numpy as jnp
|
|
29
|
+
from artifex.generative_models.core.base import MLP
|
|
30
|
+
from flax import nnx
|
|
31
|
+
from jaxtyping import Array, Float, Int
|
|
32
|
+
|
|
33
|
+
from diffbio.constants import DEFAULT_TEMPERATURE, EPSILON
|
|
34
|
+
from diffbio.utils.nn_utils import get_rng_key
|
|
35
|
+
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# Re-export from artifex (import when available, provide stubs otherwise)
|
|
38
|
+
# =============================================================================
|
|
39
|
+
|
|
40
|
+
from artifex.generative_models.core.layers.positional import (
|
|
41
|
+
PositionalEncoding,
|
|
42
|
+
RotaryPositionalEncoding as RoPE,
|
|
43
|
+
SinusoidalPositionalEncoding,
|
|
44
|
+
)
|
|
45
|
+
from artifex.generative_models.core.layers.residual import (
|
|
46
|
+
Conv1DResidualBlock as ResidualBlock1D,
|
|
47
|
+
Conv2DResidualBlock as ResidualBlock2D,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
# DiffBio-specific components
|
|
53
|
+
"GumbelSoftmaxModule",
|
|
54
|
+
"GraphMessagePassing",
|
|
55
|
+
# Re-exported from artifex
|
|
56
|
+
"PositionalEncoding",
|
|
57
|
+
"SinusoidalPositionalEncoding",
|
|
58
|
+
"RoPE",
|
|
59
|
+
"ResidualBlock1D",
|
|
60
|
+
"ResidualBlock2D",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GumbelSoftmaxModule(nnx.Module):
|
|
65
|
+
"""Neural network module for Gumbel-softmax sampling.
|
|
66
|
+
|
|
67
|
+
This module wraps the gumbel_softmax function as an nnx.Module,
|
|
68
|
+
providing differentiable categorical sampling during forward pass.
|
|
69
|
+
|
|
70
|
+
Useful for:
|
|
71
|
+
|
|
72
|
+
- Discrete latent variable models (VQ-VAE variants)
|
|
73
|
+
- Hard attention mechanisms
|
|
74
|
+
- Discrete sequence generation
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
temperature: Initial temperature for sampling.
|
|
78
|
+
hard: If True, use straight-through estimator for discrete samples.
|
|
79
|
+
rngs: Flax NNX random number generators.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
```python
|
|
83
|
+
module = GumbelSoftmaxModule(temperature=0.5, rngs=nnx.Rngs(42))
|
|
84
|
+
logits = jnp.array([[1.0, 2.0, 3.0]])
|
|
85
|
+
samples = module(logits)
|
|
86
|
+
```
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
temperature: float = DEFAULT_TEMPERATURE,
|
|
92
|
+
hard: bool = False,
|
|
93
|
+
*,
|
|
94
|
+
rngs: nnx.Rngs,
|
|
95
|
+
):
|
|
96
|
+
"""Initialize GumbelSoftmaxModule.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
temperature: Temperature for Gumbel-softmax.
|
|
100
|
+
hard: Whether to use hard (one-hot) samples.
|
|
101
|
+
rngs: Random number generators.
|
|
102
|
+
"""
|
|
103
|
+
super().__init__()
|
|
104
|
+
self.temperature = temperature
|
|
105
|
+
self.hard = hard
|
|
106
|
+
self.rngs = rngs
|
|
107
|
+
|
|
108
|
+
def __call__(self, logits: Float[Array, "... n"]) -> Float[Array, "... n"]:
|
|
109
|
+
"""Apply Gumbel-softmax sampling.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
logits: Unnormalized log-probabilities of shape (..., n).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Samples of same shape as logits.
|
|
116
|
+
"""
|
|
117
|
+
key = get_rng_key(self.rngs, "dropout", fallback_seed=0)
|
|
118
|
+
gumbel_noise = jax.random.gumbel(key, logits.shape)
|
|
119
|
+
perturbed = (logits + gumbel_noise) / self.temperature
|
|
120
|
+
soft_sample = jax.nn.softmax(perturbed, axis=-1)
|
|
121
|
+
if self.hard:
|
|
122
|
+
hard_sample = jax.nn.one_hot(
|
|
123
|
+
jnp.argmax(soft_sample, axis=-1),
|
|
124
|
+
logits.shape[-1],
|
|
125
|
+
)
|
|
126
|
+
return hard_sample - jax.lax.stop_gradient(soft_sample) + soft_sample
|
|
127
|
+
return soft_sample
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class GraphMessagePassing(nnx.Module):
|
|
131
|
+
"""Graph neural network message passing layer.
|
|
132
|
+
|
|
133
|
+
Implements a standard message passing scheme:
|
|
134
|
+
1. Compute messages from source nodes and edge features
|
|
135
|
+
2. Aggregate messages at destination nodes
|
|
136
|
+
3. Update node features with aggregated messages
|
|
137
|
+
|
|
138
|
+
Supports different aggregation functions (sum, mean, max).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
node_features: Input node feature dimension.
|
|
142
|
+
edge_features: Edge feature dimension.
|
|
143
|
+
hidden_dim: Output hidden dimension.
|
|
144
|
+
aggregation: Aggregation function ("sum", "mean", "max").
|
|
145
|
+
rngs: Flax NNX random number generators.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
```python
|
|
149
|
+
layer = GraphMessagePassing(
|
|
150
|
+
node_features=32, edge_features=8, hidden_dim=64,
|
|
151
|
+
rngs=nnx.Rngs(42)
|
|
152
|
+
)
|
|
153
|
+
node_feat = jnp.ones((5, 32)) # 5 nodes
|
|
154
|
+
edge_feat = jnp.ones((8, 8)) # 8 edges
|
|
155
|
+
edge_index = jnp.array([[0,0,1,1,2,2,3,4], [1,2,2,3,3,4,4,0]])
|
|
156
|
+
output = layer(node_feat, edge_feat, edge_index)
|
|
157
|
+
```
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
node_features: int,
|
|
163
|
+
edge_features: int,
|
|
164
|
+
hidden_dim: int,
|
|
165
|
+
aggregation: Literal["sum", "mean", "max"] = "sum",
|
|
166
|
+
*,
|
|
167
|
+
rngs: nnx.Rngs,
|
|
168
|
+
):
|
|
169
|
+
"""Initialize GraphMessagePassing layer.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
node_features: Input node feature dimension.
|
|
173
|
+
edge_features: Edge feature dimension.
|
|
174
|
+
hidden_dim: Output dimension.
|
|
175
|
+
aggregation: Aggregation method.
|
|
176
|
+
rngs: Random number generators.
|
|
177
|
+
"""
|
|
178
|
+
super().__init__()
|
|
179
|
+
if node_features <= 0:
|
|
180
|
+
raise ValueError(f"node_features must be positive, got {node_features}")
|
|
181
|
+
if edge_features <= 0:
|
|
182
|
+
raise ValueError(f"edge_features must be positive, got {edge_features}")
|
|
183
|
+
if hidden_dim <= 0:
|
|
184
|
+
raise ValueError(f"hidden_dim must be positive, got {hidden_dim}")
|
|
185
|
+
if aggregation not in {"sum", "mean", "max"}:
|
|
186
|
+
raise ValueError(f"Unknown aggregation: {aggregation}")
|
|
187
|
+
|
|
188
|
+
self.hidden_dim = nnx.static(hidden_dim)
|
|
189
|
+
self.aggregation = nnx.static(aggregation)
|
|
190
|
+
self.message_mlp = MLP(
|
|
191
|
+
[hidden_dim, hidden_dim],
|
|
192
|
+
in_features=node_features + edge_features,
|
|
193
|
+
activation=nnx.relu,
|
|
194
|
+
rngs=rngs,
|
|
195
|
+
)
|
|
196
|
+
self.update_mlp = MLP(
|
|
197
|
+
[hidden_dim, hidden_dim],
|
|
198
|
+
in_features=node_features + hidden_dim,
|
|
199
|
+
activation=nnx.relu,
|
|
200
|
+
rngs=rngs,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def __call__(
|
|
204
|
+
self,
|
|
205
|
+
node_features: Float[Array, "num_nodes node_feat"],
|
|
206
|
+
edge_features: Float[Array, "num_edges edge_feat"],
|
|
207
|
+
edge_index: Int[Array, "2 num_edges"],
|
|
208
|
+
) -> Float[Array, "num_nodes hidden_dim"]:
|
|
209
|
+
"""Apply message passing.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
node_features: Node feature matrix (num_nodes, node_features).
|
|
213
|
+
edge_features: Edge feature matrix (num_edges, edge_features).
|
|
214
|
+
edge_index: Edge indices [source, dest] of shape (2, num_edges).
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Updated node features (num_nodes, hidden_dim).
|
|
218
|
+
"""
|
|
219
|
+
num_nodes = node_features.shape[0]
|
|
220
|
+
num_edges = edge_index.shape[1]
|
|
221
|
+
|
|
222
|
+
# Handle empty graph case
|
|
223
|
+
if num_edges == 0:
|
|
224
|
+
# No messages, just transform node features
|
|
225
|
+
update_output = self.update_mlp(
|
|
226
|
+
jnp.concatenate([node_features, jnp.zeros((num_nodes, self.hidden_dim))], axis=-1)
|
|
227
|
+
)
|
|
228
|
+
if isinstance(update_output, tuple):
|
|
229
|
+
return update_output[0]
|
|
230
|
+
return update_output
|
|
231
|
+
|
|
232
|
+
# Extract source and destination node indices
|
|
233
|
+
source_idx = edge_index[0]
|
|
234
|
+
dest_idx = edge_index[1]
|
|
235
|
+
|
|
236
|
+
# Get source node features for each edge
|
|
237
|
+
source_features = node_features[source_idx] # (num_edges, node_feat)
|
|
238
|
+
|
|
239
|
+
# Compute messages: MLP(concat(source_features, edge_features))
|
|
240
|
+
message_input = jnp.concatenate([source_features, edge_features], axis=-1)
|
|
241
|
+
messages = self.message_mlp(message_input)
|
|
242
|
+
if isinstance(messages, tuple):
|
|
243
|
+
messages = messages[0]
|
|
244
|
+
|
|
245
|
+
# Aggregate messages at destination nodes
|
|
246
|
+
if self.aggregation == "sum":
|
|
247
|
+
aggregated = jax.ops.segment_sum(messages, dest_idx, num_segments=num_nodes)
|
|
248
|
+
elif self.aggregation == "mean":
|
|
249
|
+
sum_messages = jax.ops.segment_sum(messages, dest_idx, num_segments=num_nodes)
|
|
250
|
+
counts = jax.ops.segment_sum(jnp.ones(num_edges), dest_idx, num_segments=num_nodes)
|
|
251
|
+
aggregated = sum_messages / (counts[:, None] + EPSILON)
|
|
252
|
+
elif self.aggregation == "max":
|
|
253
|
+
# segment_max with default of -inf for empty segments
|
|
254
|
+
aggregated = jax.ops.segment_max(
|
|
255
|
+
messages,
|
|
256
|
+
dest_idx,
|
|
257
|
+
num_segments=num_nodes,
|
|
258
|
+
indices_are_sorted=False,
|
|
259
|
+
)
|
|
260
|
+
# Replace -inf with 0 for nodes with no incoming edges
|
|
261
|
+
aggregated = jnp.where(jnp.isinf(aggregated), jnp.zeros_like(aggregated), aggregated)
|
|
262
|
+
else:
|
|
263
|
+
raise ValueError(f"Unknown aggregation: {self.aggregation}")
|
|
264
|
+
|
|
265
|
+
# Update node features: MLP(concat(node_features, aggregated))
|
|
266
|
+
update_input = jnp.concatenate([node_features, aggregated], axis=-1)
|
|
267
|
+
updated = self.update_mlp(update_input)
|
|
268
|
+
if isinstance(updated, tuple):
|
|
269
|
+
return updated[0]
|
|
270
|
+
return updated
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Optimal transport layers for differentiable assignment and matching.
|
|
2
|
+
|
|
3
|
+
Ownership note: DiffBio retains this transport-plan layer because downstream
|
|
4
|
+
operators need a differentiable marginal-constrained plan module. Calibrax
|
|
5
|
+
currently exposes scalar Sinkhorn-style metrics, not this operator contract.
|
|
6
|
+
|
|
7
|
+
This module provides differentiable optimal transport solvers using the
|
|
8
|
+
Sinkhorn algorithm in log-domain for numerical stability.
|
|
9
|
+
|
|
10
|
+
Components:
|
|
11
|
+
|
|
12
|
+
- **SinkhornLayer**: Computes the entropy-regularised optimal transport plan
|
|
13
|
+
between two discrete distributions given a cost matrix, using the
|
|
14
|
+
Sinkhorn-Knopp algorithm in log-domain.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import jax
|
|
18
|
+
import jax.numpy as jnp
|
|
19
|
+
from flax import nnx
|
|
20
|
+
from jaxtyping import Array, Float
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"SinkhornLayer",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SinkhornLayer(nnx.Module):
|
|
28
|
+
"""Sinkhorn optimal transport layer (log-domain).
|
|
29
|
+
|
|
30
|
+
Computes the entropy-regularised optimal transport plan between two
|
|
31
|
+
discrete marginal distributions ``a`` and ``b`` given a cost matrix ``C``,
|
|
32
|
+
by solving::
|
|
33
|
+
|
|
34
|
+
min_{P >= 0} <P, C> - epsilon * H(P)
|
|
35
|
+
s.t. P @ 1 = a, P^T @ 1 = b
|
|
36
|
+
|
|
37
|
+
The algorithm runs in log-domain for numerical stability::
|
|
38
|
+
|
|
39
|
+
f, g = 0, 0
|
|
40
|
+
for _ in range(num_iters):
|
|
41
|
+
f = epsilon * log(a) - epsilon * logsumexp((-C + g) / epsilon, axis=1)
|
|
42
|
+
g = epsilon * log(b) - epsilon * logsumexp((-C + f) / epsilon, axis=0)
|
|
43
|
+
P = exp((f[:, None] + g[None, :] - C) / epsilon)
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
epsilon: Entropy regularisation strength (larger = smoother plan).
|
|
47
|
+
num_iters: Number of Sinkhorn iterations.
|
|
48
|
+
rngs: Flax NNX random number generators (unused, kept for API consistency).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
epsilon: float,
|
|
54
|
+
num_iters: int,
|
|
55
|
+
*,
|
|
56
|
+
rngs: nnx.Rngs,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Initialize the Sinkhorn layer.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
epsilon: Regularisation strength.
|
|
62
|
+
num_iters: Number of Sinkhorn iterations.
|
|
63
|
+
rngs: Random number generators (for API consistency).
|
|
64
|
+
"""
|
|
65
|
+
super().__init__()
|
|
66
|
+
self.epsilon = nnx.static(epsilon)
|
|
67
|
+
self.num_iters = nnx.static(num_iters)
|
|
68
|
+
|
|
69
|
+
def __call__(
|
|
70
|
+
self,
|
|
71
|
+
cost: Float[Array, "n m"],
|
|
72
|
+
a: Float[Array, " n"],
|
|
73
|
+
b: Float[Array, " m"],
|
|
74
|
+
) -> Float[Array, "n m"]:
|
|
75
|
+
"""Compute the optimal transport plan.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
cost: Cost matrix of shape ``(n, m)``.
|
|
79
|
+
a: Source marginal distribution of shape ``(n,)``, must sum to 1.
|
|
80
|
+
b: Target marginal distribution of shape ``(m,)``, must sum to 1.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Transport plan of shape ``(n, m)`` satisfying (approximately)
|
|
84
|
+
``P @ 1 = a`` and ``P^T @ 1 = b``.
|
|
85
|
+
"""
|
|
86
|
+
return _sinkhorn_log_domain(cost, a, b, self.epsilon, self.num_iters)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _sinkhorn_log_domain(
|
|
90
|
+
cost: Float[Array, "n m"],
|
|
91
|
+
a: Float[Array, " n"],
|
|
92
|
+
b: Float[Array, " m"],
|
|
93
|
+
epsilon: float,
|
|
94
|
+
num_iters: int,
|
|
95
|
+
) -> Float[Array, "n m"]:
|
|
96
|
+
"""Run the Sinkhorn algorithm in log-domain.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
cost: Cost matrix ``(n, m)``.
|
|
100
|
+
a: Source marginal ``(n,)``.
|
|
101
|
+
b: Target marginal ``(m,)``.
|
|
102
|
+
epsilon: Regularisation parameter.
|
|
103
|
+
num_iters: Number of iterations.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Transport plan ``(n, m)``.
|
|
107
|
+
"""
|
|
108
|
+
log_a = jnp.log(a + 1e-30)
|
|
109
|
+
log_b = jnp.log(b + 1e-30)
|
|
110
|
+
|
|
111
|
+
f = jnp.zeros_like(a)
|
|
112
|
+
g = jnp.zeros_like(b)
|
|
113
|
+
|
|
114
|
+
def _step(
|
|
115
|
+
carry: tuple[Float[Array, " n"], Float[Array, " m"]], _: None
|
|
116
|
+
) -> tuple[tuple[Float[Array, " n"], Float[Array, " m"]], None]:
|
|
117
|
+
"""Perform one Sinkhorn iteration updating dual variables f and g."""
|
|
118
|
+
f_prev, g_prev = carry
|
|
119
|
+
# f update: epsilon * log(a) - epsilon * logsumexp((-C + g) / epsilon, axis=1)
|
|
120
|
+
f_new = epsilon * log_a - epsilon * jax.scipy.special.logsumexp(
|
|
121
|
+
(-cost + g_prev[None, :]) / epsilon, axis=1
|
|
122
|
+
)
|
|
123
|
+
# g update: epsilon * log(b) - epsilon * logsumexp((-C + f) / epsilon, axis=0)
|
|
124
|
+
g_new = epsilon * log_b - epsilon * jax.scipy.special.logsumexp(
|
|
125
|
+
(-cost + f_new[:, None]) / epsilon, axis=0
|
|
126
|
+
)
|
|
127
|
+
return (f_new, g_new), None
|
|
128
|
+
|
|
129
|
+
(f, g), _ = jax.lax.scan(_step, (f, g), None, length=num_iters)
|
|
130
|
+
|
|
131
|
+
# Recover the transport plan
|
|
132
|
+
log_plan = (f[:, None] + g[None, :] - cost) / epsilon
|
|
133
|
+
return jnp.exp(log_plan)
|