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,387 @@
|
|
|
1
|
+
"""Single-cell specific loss functions for differentiable bioinformatics.
|
|
2
|
+
|
|
3
|
+
This module provides differentiable loss functions for single-cell analysis
|
|
4
|
+
pipelines, including batch correction, clustering, RNA velocity, and diversity.
|
|
5
|
+
|
|
6
|
+
Includes:
|
|
7
|
+
- BatchMixingLoss: Maximizes batch mixing in latent space
|
|
8
|
+
- ClusteringCompactnessLoss: Encourages tight, well-separated clusters
|
|
9
|
+
- VelocityConsistencyLoss: Enforces consistency between velocity and trajectory
|
|
10
|
+
- ShannonDiversityLoss: Shannon entropy of soft cluster assignments
|
|
11
|
+
- SimpsonDiversityLoss: Simpson concentration index of soft cluster assignments
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import jax
|
|
15
|
+
import jax.numpy as jnp
|
|
16
|
+
from calibrax.metrics.functional.information import entropy
|
|
17
|
+
from flax import nnx
|
|
18
|
+
from jaxtyping import Array, Float, Int
|
|
19
|
+
|
|
20
|
+
from diffbio.constants import DISTANCE_MASK_SENTINEL
|
|
21
|
+
from diffbio.core import soft_ops
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BatchMixingLoss(nnx.Module):
|
|
25
|
+
"""Loss function to maximize batch mixing in latent space.
|
|
26
|
+
|
|
27
|
+
Computes how well batches are mixed in the embedding space by measuring
|
|
28
|
+
the entropy of batch labels among k-nearest neighbors for each cell.
|
|
29
|
+
Higher entropy indicates better mixing.
|
|
30
|
+
|
|
31
|
+
The loss encourages the model to learn representations where cells from
|
|
32
|
+
different batches are interleaved, reducing batch effects.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
n_neighbors: Number of nearest neighbors to consider.
|
|
36
|
+
n_batches: Number of batches (required for JIT compatibility).
|
|
37
|
+
temperature: Temperature for softmax in distance computation.
|
|
38
|
+
rngs: Flax NNX random number generators.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
```python
|
|
42
|
+
loss_fn = BatchMixingLoss(n_neighbors=15, n_batches=3, rngs=nnx.Rngs(42))
|
|
43
|
+
loss = loss_fn(embeddings, batch_labels)
|
|
44
|
+
```
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
n_neighbors: int = 15,
|
|
50
|
+
n_batches: int = 3,
|
|
51
|
+
temperature: float = 1.0,
|
|
52
|
+
*,
|
|
53
|
+
rngs: nnx.Rngs | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Initialize the batch mixing loss.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
n_neighbors: Number of nearest neighbors to consider.
|
|
59
|
+
n_batches: Number of batches (static for JIT compatibility).
|
|
60
|
+
temperature: Temperature for soft neighbor selection.
|
|
61
|
+
rngs: Random number generators (not used, for API consistency).
|
|
62
|
+
"""
|
|
63
|
+
super().__init__()
|
|
64
|
+
self.n_neighbors = n_neighbors
|
|
65
|
+
self.n_batches = n_batches
|
|
66
|
+
self.temperature = temperature
|
|
67
|
+
# Precompute max entropy for normalization (static constant)
|
|
68
|
+
self._max_entropy = jnp.log(jnp.array(n_batches, dtype=jnp.float32))
|
|
69
|
+
|
|
70
|
+
def __call__(
|
|
71
|
+
self,
|
|
72
|
+
embeddings: Float[Array, "n_cells latent_dim"],
|
|
73
|
+
batch_labels: Int[Array, "n_cells"],
|
|
74
|
+
) -> Float[Array, ""]:
|
|
75
|
+
"""Compute batch mixing loss.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
embeddings: Cell embeddings in latent space.
|
|
79
|
+
batch_labels: Integer batch label for each cell.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Negative mean entropy of batch distribution in neighborhoods (scalar).
|
|
83
|
+
Lower loss means better mixing.
|
|
84
|
+
"""
|
|
85
|
+
n_cells = embeddings.shape[0]
|
|
86
|
+
n_batches = self.n_batches
|
|
87
|
+
|
|
88
|
+
# Compute pairwise distances
|
|
89
|
+
# ||a - b||^2 = ||a||^2 + ||b||^2 - 2 * a.b
|
|
90
|
+
sq_norms = jnp.sum(embeddings**2, axis=-1)
|
|
91
|
+
distances = sq_norms[:, None] + sq_norms[None, :] - 2 * embeddings @ embeddings.T
|
|
92
|
+
|
|
93
|
+
# Set self-distance to inf to exclude self from neighbors
|
|
94
|
+
distances = distances + jnp.eye(n_cells) * DISTANCE_MASK_SENTINEL
|
|
95
|
+
|
|
96
|
+
# Soft neighbor weights using softmax over negative distances
|
|
97
|
+
neighbor_weights = jax.nn.softmax(-distances / self.temperature, axis=-1)
|
|
98
|
+
|
|
99
|
+
# Keep only top k neighbors (soft selection)
|
|
100
|
+
# Sort to get top-k, then create soft mask
|
|
101
|
+
sorted_dists = soft_ops.sort(distances, axis=-1, softness=self.temperature)
|
|
102
|
+
kth_dist = sorted_dists[:, self.n_neighbors - 1 : self.n_neighbors]
|
|
103
|
+
k_mask = soft_ops.less(distances, kth_dist, softness=self.temperature)
|
|
104
|
+
|
|
105
|
+
# Apply mask
|
|
106
|
+
neighbor_weights = neighbor_weights * k_mask
|
|
107
|
+
neighbor_weights = neighbor_weights / (neighbor_weights.sum(axis=-1, keepdims=True) + 1e-8)
|
|
108
|
+
|
|
109
|
+
# One-hot encode batch labels
|
|
110
|
+
batch_onehot = jax.nn.one_hot(batch_labels, n_batches)
|
|
111
|
+
|
|
112
|
+
# Compute batch distribution in neighborhoods
|
|
113
|
+
# For each cell, weighted average of neighbor batch labels
|
|
114
|
+
batch_dist = neighbor_weights @ batch_onehot # (n_cells, n_batches)
|
|
115
|
+
|
|
116
|
+
# Compute entropy of batch distribution
|
|
117
|
+
# H = -sum(p * log(p))
|
|
118
|
+
eps = 1e-8
|
|
119
|
+
per_cell_entropy = -jnp.sum(batch_dist * jnp.log(batch_dist + eps), axis=-1)
|
|
120
|
+
|
|
121
|
+
# Normalized entropy (0 to 1, higher is better)
|
|
122
|
+
# Use precomputed max_entropy for JIT compatibility
|
|
123
|
+
normalized_entropy = per_cell_entropy / (self._max_entropy + eps)
|
|
124
|
+
|
|
125
|
+
# Return negative mean entropy (lower loss = better mixing)
|
|
126
|
+
return -jnp.mean(normalized_entropy)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ClusteringCompactnessLoss(nnx.Module):
|
|
130
|
+
"""Loss function to encourage compact and well-separated clusters.
|
|
131
|
+
|
|
132
|
+
Combines two components:
|
|
133
|
+
1. Compactness: Minimize within-cluster variance
|
|
134
|
+
2. Separation: Maximize between-cluster distances
|
|
135
|
+
|
|
136
|
+
Works with soft cluster assignments for end-to-end differentiability.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
separation_weight: Weight for the separation term.
|
|
140
|
+
min_separation: Minimum desired distance between cluster centers.
|
|
141
|
+
rngs: Flax NNX random number generators.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
```python
|
|
145
|
+
loss_fn = ClusteringCompactnessLoss(rngs=nnx.Rngs(42))
|
|
146
|
+
loss = loss_fn(embeddings, soft_assignments)
|
|
147
|
+
```
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
separation_weight: float = 1.0,
|
|
153
|
+
min_separation: float = 1.0,
|
|
154
|
+
*,
|
|
155
|
+
rngs: nnx.Rngs | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Initialize the clustering compactness loss.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
separation_weight: Weight for separation term.
|
|
161
|
+
min_separation: Minimum desired distance between centroids.
|
|
162
|
+
rngs: Random number generators (not used, for API consistency).
|
|
163
|
+
"""
|
|
164
|
+
super().__init__()
|
|
165
|
+
self.separation_weight = separation_weight
|
|
166
|
+
self.min_separation = min_separation
|
|
167
|
+
|
|
168
|
+
def __call__(
|
|
169
|
+
self,
|
|
170
|
+
embeddings: Float[Array, "n_cells latent_dim"],
|
|
171
|
+
assignments: Float[Array, "n_cells n_clusters"],
|
|
172
|
+
centroids: Float[Array, "n_clusters latent_dim"] | None = None,
|
|
173
|
+
) -> Float[Array, ""]:
|
|
174
|
+
"""Compute clustering compactness loss.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
embeddings: Cell embeddings in latent space.
|
|
178
|
+
assignments: Soft cluster assignments (should sum to 1 per cell).
|
|
179
|
+
centroids: Optional cluster centroids. If provided, uses these directly
|
|
180
|
+
for gradient flow. If None, computes soft centroids from assignments.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Combined compactness and separation loss (scalar).
|
|
184
|
+
"""
|
|
185
|
+
n_clusters = assignments.shape[1]
|
|
186
|
+
|
|
187
|
+
# Use provided centroids or compute soft centroids from assignments
|
|
188
|
+
if centroids is None:
|
|
189
|
+
# Compute soft cluster centroids
|
|
190
|
+
# centroid_k = sum_i(assignment_ik * embedding_i) / sum_i(assignment_ik)
|
|
191
|
+
assignment_sums = assignments.sum(axis=0, keepdims=True).T # (n_clusters, 1)
|
|
192
|
+
centroids = (assignments.T @ embeddings) / (
|
|
193
|
+
assignment_sums + 1e-8
|
|
194
|
+
) # (n_clusters, latent_dim)
|
|
195
|
+
|
|
196
|
+
# Compactness: weighted sum of squared distances to centroids
|
|
197
|
+
# For each cell, compute distance to each centroid
|
|
198
|
+
# Then weight by assignment
|
|
199
|
+
distances_to_centroids = jnp.sum(
|
|
200
|
+
(embeddings[:, None, :] - centroids[None, :, :]) ** 2, axis=-1
|
|
201
|
+
) # (n_cells, n_clusters)
|
|
202
|
+
|
|
203
|
+
# Weighted compactness
|
|
204
|
+
compactness = jnp.sum(assignments * distances_to_centroids) / embeddings.shape[0]
|
|
205
|
+
|
|
206
|
+
# Separation: pairwise distances between centroids
|
|
207
|
+
# We want centroids to be at least min_separation apart
|
|
208
|
+
centroid_dists = jnp.sqrt(
|
|
209
|
+
jnp.sum((centroids[:, None, :] - centroids[None, :, :]) ** 2, axis=-1) + 1e-8
|
|
210
|
+
) # (n_clusters, n_clusters)
|
|
211
|
+
|
|
212
|
+
# Hinge loss: penalize if distance < min_separation
|
|
213
|
+
# Exclude diagonal (self-distance)
|
|
214
|
+
mask = 1.0 - jnp.eye(n_clusters)
|
|
215
|
+
separation_violations = (
|
|
216
|
+
soft_ops.relu(self.min_separation - centroid_dists, softness=0.1) * mask
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Mean separation loss (excluding diagonal)
|
|
220
|
+
n_pairs = n_clusters * (n_clusters - 1)
|
|
221
|
+
separation_loss = jnp.sum(separation_violations) / (n_pairs + 1e-8)
|
|
222
|
+
|
|
223
|
+
# Combined loss
|
|
224
|
+
return compactness + self.separation_weight * separation_loss
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class VelocityConsistencyLoss(nnx.Module):
|
|
228
|
+
"""Loss function to enforce consistency between velocity and trajectory.
|
|
229
|
+
|
|
230
|
+
Ensures that the predicted RNA velocity is consistent with actual
|
|
231
|
+
expression changes over time. Combines directional (cosine) and
|
|
232
|
+
magnitude consistency.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
dt: Time step for velocity extrapolation.
|
|
236
|
+
cosine_weight: Weight for directional consistency.
|
|
237
|
+
magnitude_weight: Weight for magnitude consistency.
|
|
238
|
+
rngs: Flax NNX random number generators.
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
```python
|
|
242
|
+
loss_fn = VelocityConsistencyLoss(rngs=nnx.Rngs(42))
|
|
243
|
+
loss = loss_fn(expression, velocity, future_expression)
|
|
244
|
+
```
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
def __init__(
|
|
248
|
+
self,
|
|
249
|
+
dt: float = 0.1,
|
|
250
|
+
cosine_weight: float = 1.0,
|
|
251
|
+
magnitude_weight: float = 1.0,
|
|
252
|
+
*,
|
|
253
|
+
rngs: nnx.Rngs | None = None,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Initialize the velocity consistency loss.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
dt: Time step for velocity extrapolation.
|
|
259
|
+
cosine_weight: Weight for cosine similarity loss.
|
|
260
|
+
magnitude_weight: Weight for magnitude loss.
|
|
261
|
+
rngs: Random number generators (not used, for API consistency).
|
|
262
|
+
"""
|
|
263
|
+
super().__init__()
|
|
264
|
+
self.dt = dt
|
|
265
|
+
self.cosine_weight = cosine_weight
|
|
266
|
+
self.magnitude_weight = magnitude_weight
|
|
267
|
+
|
|
268
|
+
def __call__(
|
|
269
|
+
self,
|
|
270
|
+
expression: Float[Array, "n_cells n_genes"],
|
|
271
|
+
velocity: Float[Array, "n_cells n_genes"],
|
|
272
|
+
future_expression: Float[Array, "n_cells n_genes"],
|
|
273
|
+
) -> Float[Array, ""]:
|
|
274
|
+
"""Compute velocity consistency loss.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
expression: Current gene expression.
|
|
278
|
+
velocity: Predicted RNA velocity (rate of change).
|
|
279
|
+
future_expression: Future gene expression (ground truth or estimated).
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Combined directional and magnitude consistency loss (scalar).
|
|
283
|
+
"""
|
|
284
|
+
# Predicted change based on velocity
|
|
285
|
+
predicted_delta = velocity * self.dt
|
|
286
|
+
|
|
287
|
+
# Actual change
|
|
288
|
+
actual_delta = future_expression - expression
|
|
289
|
+
|
|
290
|
+
# Cosine similarity loss (directional consistency)
|
|
291
|
+
# cosine_sim = (a . b) / (||a|| * ||b||)
|
|
292
|
+
eps = 1e-8
|
|
293
|
+
pred_norm = jnp.sqrt(jnp.sum(predicted_delta**2, axis=-1) + eps)
|
|
294
|
+
actual_norm = jnp.sqrt(jnp.sum(actual_delta**2, axis=-1) + eps)
|
|
295
|
+
dot_product = jnp.sum(predicted_delta * actual_delta, axis=-1)
|
|
296
|
+
|
|
297
|
+
cosine_sim = dot_product / (pred_norm * actual_norm)
|
|
298
|
+
|
|
299
|
+
# Cosine loss: 1 - cosine_sim (ranges from 0 to 2)
|
|
300
|
+
cosine_loss = jnp.mean(1 - cosine_sim)
|
|
301
|
+
|
|
302
|
+
# Magnitude loss: MSE between predicted and actual delta magnitudes
|
|
303
|
+
magnitude_loss = jnp.mean((pred_norm - actual_norm) ** 2)
|
|
304
|
+
|
|
305
|
+
# Combined loss
|
|
306
|
+
return self.cosine_weight * cosine_loss + self.magnitude_weight * magnitude_loss
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class ShannonDiversityLoss(nnx.Module):
|
|
310
|
+
"""Mean Shannon entropy of soft cluster assignments across cells.
|
|
311
|
+
|
|
312
|
+
Measures assignment diversity using Shannon entropy. Higher values indicate
|
|
313
|
+
more uniform (diverse) cluster assignments, while lower values indicate
|
|
314
|
+
concentrated assignments.
|
|
315
|
+
|
|
316
|
+
Delegates to ``calibrax.metrics.functional.information.entropy`` for the
|
|
317
|
+
per-cell entropy computation.
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
```python
|
|
321
|
+
loss_fn = ShannonDiversityLoss()
|
|
322
|
+
# Soft cluster probabilities: (n_cells, n_clusters)
|
|
323
|
+
assignments = jax.nn.softmax(logits, axis=-1)
|
|
324
|
+
diversity = loss_fn(assignments) # scalar, higher = more diverse
|
|
325
|
+
```
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
def __init__(self) -> None:
|
|
329
|
+
"""Initialize the Shannon diversity loss."""
|
|
330
|
+
super().__init__()
|
|
331
|
+
|
|
332
|
+
def __call__(
|
|
333
|
+
self,
|
|
334
|
+
assignments: Float[Array, "n_cells n_clusters"],
|
|
335
|
+
) -> Float[Array, ""]:
|
|
336
|
+
"""Compute mean Shannon entropy of soft cluster assignments.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
assignments: Soft cluster probabilities of shape ``(n_cells, n_clusters)``.
|
|
340
|
+
Each row should sum to 1.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Mean Shannon entropy across cells (scalar). Range ``[0, log(K)]``
|
|
344
|
+
where ``K`` is the number of clusters.
|
|
345
|
+
"""
|
|
346
|
+
# Compute per-cell Shannon entropy via calibrax, then average
|
|
347
|
+
per_cell_entropy = jax.vmap(entropy)(assignments)
|
|
348
|
+
return jnp.mean(per_cell_entropy)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class SimpsonDiversityLoss(nnx.Module):
|
|
352
|
+
"""Mean Simpson concentration index of soft cluster assignments.
|
|
353
|
+
|
|
354
|
+
Computes the sum of squared assignment probabilities per cell, averaged
|
|
355
|
+
across all cells. Lower values indicate more diverse (uniform) assignments.
|
|
356
|
+
|
|
357
|
+
- Uniform assignments over K clusters yield ``1/K``.
|
|
358
|
+
- Fully concentrated (one-hot) assignments yield ``1.0``.
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
```python
|
|
362
|
+
loss_fn = SimpsonDiversityLoss()
|
|
363
|
+
assignments = jax.nn.softmax(logits, axis=-1)
|
|
364
|
+
concentration = loss_fn(assignments) # scalar, lower = more diverse
|
|
365
|
+
```
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
def __init__(self) -> None:
|
|
369
|
+
"""Initialize the Simpson diversity loss."""
|
|
370
|
+
super().__init__()
|
|
371
|
+
|
|
372
|
+
def __call__(
|
|
373
|
+
self,
|
|
374
|
+
assignments: Float[Array, "n_cells n_clusters"],
|
|
375
|
+
) -> Float[Array, ""]:
|
|
376
|
+
"""Compute mean Simpson concentration index.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
assignments: Soft cluster probabilities of shape ``(n_cells, n_clusters)``.
|
|
380
|
+
Each row should sum to 1.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Mean sum-of-squared-probabilities across cells (scalar).
|
|
384
|
+
Range ``[1/K, 1.0]`` where ``K`` is the number of clusters.
|
|
385
|
+
"""
|
|
386
|
+
per_cell_simpson = jnp.sum(assignments**2, axis=-1)
|
|
387
|
+
return jnp.mean(per_cell_simpson)
|