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,629 @@
|
|
|
1
|
+
"""Shared graph attention components for GNN-based operators.
|
|
2
|
+
|
|
3
|
+
Ownership note: DiffBio retains these sparse GAT/GATv2 layers because sibling
|
|
4
|
+
repos do not currently expose a GATv2-compatible graph-attention block with the
|
|
5
|
+
same edge-feature and segment-softmax contract. Generic model pieces should
|
|
6
|
+
still come from Artifex where an exact reusable layer exists.
|
|
7
|
+
|
|
8
|
+
This module provides reusable graph attention building blocks that are needed
|
|
9
|
+
by multiple downstream operators (assembly, cell-cell communication, GRN
|
|
10
|
+
inference, spatial domain identification, etc.).
|
|
11
|
+
|
|
12
|
+
Components:
|
|
13
|
+
|
|
14
|
+
- **GraphAttentionLayer**: Multi-head GAT-style attention (dot-product Q*K).
|
|
15
|
+
- **GraphAttentionBlock**: GAT attention + LayerNorm + residual + FFN.
|
|
16
|
+
- **GATv2Layer**: GATv2-style attention that applies LeakyReLU *before* the
|
|
17
|
+
attention vector dot product, making it strictly more expressive than GAT.
|
|
18
|
+
- **GATv2Block**: GATv2 attention + LayerNorm + residual + FFN.
|
|
19
|
+
|
|
20
|
+
These are architecturally distinct from ``GraphMessagePassing`` in
|
|
21
|
+
``neural_components.py``, which uses simpler sum/mean/max aggregation
|
|
22
|
+
without attention.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import jax
|
|
26
|
+
import jax.numpy as jnp
|
|
27
|
+
from flax import nnx
|
|
28
|
+
from jaxtyping import Array, Float, Int
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"GraphAttentionLayer",
|
|
32
|
+
"GraphAttentionBlock",
|
|
33
|
+
"GATv2Layer",
|
|
34
|
+
"GATv2Block",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _BidirectionalProjection(nnx.Module):
|
|
39
|
+
"""Pair of linear projections used by attention mechanisms."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
in_features: int,
|
|
44
|
+
out_features: int,
|
|
45
|
+
*,
|
|
46
|
+
rngs: nnx.Rngs,
|
|
47
|
+
negative_slope: float | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Initialize the paired projection module."""
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.left = nnx.Linear(in_features=in_features, out_features=out_features, rngs=rngs)
|
|
52
|
+
self.right = nnx.Linear(in_features=in_features, out_features=out_features, rngs=rngs)
|
|
53
|
+
self.negative_slope = negative_slope
|
|
54
|
+
|
|
55
|
+
def __call__(
|
|
56
|
+
self,
|
|
57
|
+
node_features: Float[Array, "n_nodes in_features"],
|
|
58
|
+
) -> tuple[
|
|
59
|
+
Float[Array, "n_nodes out_features"],
|
|
60
|
+
Float[Array, "n_nodes out_features"],
|
|
61
|
+
]:
|
|
62
|
+
"""Project the same node tensor through both linear paths."""
|
|
63
|
+
return self.left(node_features), self.right(node_features)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GraphAttentionLayer(nnx.Module):
|
|
67
|
+
"""Multi-head graph attention layer for message passing.
|
|
68
|
+
|
|
69
|
+
Computes attention-weighted message aggregation over graph edges.
|
|
70
|
+
Each attention head independently computes query/key/value projections,
|
|
71
|
+
adds an edge-feature bias to attention scores, normalizes via
|
|
72
|
+
segment-softmax, and aggregates weighted values per target node.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
in_features: Input node feature dimension.
|
|
76
|
+
out_features: Output feature dimension (must be divisible by num_heads).
|
|
77
|
+
num_heads: Number of parallel attention heads.
|
|
78
|
+
edge_features: Edge feature dimension.
|
|
79
|
+
dropout_rate: Dropout rate applied to attention weights.
|
|
80
|
+
rngs: Flax NNX random number generators.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
in_features: int,
|
|
86
|
+
out_features: int,
|
|
87
|
+
num_heads: int,
|
|
88
|
+
edge_features: int,
|
|
89
|
+
dropout_rate: float,
|
|
90
|
+
*,
|
|
91
|
+
rngs: nnx.Rngs,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Initialize the graph attention layer.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
in_features: Input feature dimension.
|
|
97
|
+
out_features: Output feature dimension.
|
|
98
|
+
num_heads: Number of attention heads.
|
|
99
|
+
edge_features: Edge feature dimension.
|
|
100
|
+
dropout_rate: Dropout rate.
|
|
101
|
+
rngs: Random number generators.
|
|
102
|
+
"""
|
|
103
|
+
super().__init__()
|
|
104
|
+
if num_heads <= 0:
|
|
105
|
+
raise ValueError("num_heads must be positive")
|
|
106
|
+
if out_features <= 0:
|
|
107
|
+
raise ValueError("out_features must be positive")
|
|
108
|
+
if out_features % num_heads != 0:
|
|
109
|
+
raise ValueError("out_features must be divisible by num_heads")
|
|
110
|
+
if edge_features <= 0:
|
|
111
|
+
raise ValueError("edge_features must be positive")
|
|
112
|
+
if not 0.0 <= dropout_rate < 1.0:
|
|
113
|
+
raise ValueError("dropout_rate must be in [0, 1)")
|
|
114
|
+
|
|
115
|
+
# Node feature projections
|
|
116
|
+
self.query_proj = nnx.Linear(
|
|
117
|
+
in_features=in_features,
|
|
118
|
+
out_features=out_features,
|
|
119
|
+
rngs=rngs,
|
|
120
|
+
)
|
|
121
|
+
self.key_proj = nnx.Linear(
|
|
122
|
+
in_features=in_features,
|
|
123
|
+
out_features=out_features,
|
|
124
|
+
rngs=rngs,
|
|
125
|
+
)
|
|
126
|
+
self.value_proj = nnx.Linear(
|
|
127
|
+
in_features=in_features,
|
|
128
|
+
out_features=out_features,
|
|
129
|
+
rngs=rngs,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Edge feature projection
|
|
133
|
+
self.edge_proj = nnx.Linear(
|
|
134
|
+
in_features=edge_features,
|
|
135
|
+
out_features=num_heads,
|
|
136
|
+
rngs=rngs,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Output projection
|
|
140
|
+
self.output_proj = nnx.Linear(
|
|
141
|
+
in_features=out_features,
|
|
142
|
+
out_features=out_features,
|
|
143
|
+
rngs=rngs,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.dropout = nnx.Dropout(rate=dropout_rate, rngs=rngs) if dropout_rate > 0 else None
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def num_heads(self) -> int:
|
|
150
|
+
"""Number of attention heads."""
|
|
151
|
+
return self.edge_proj.out_features
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def head_dim(self) -> int:
|
|
155
|
+
"""Per-head hidden dimension."""
|
|
156
|
+
return self.query_proj.out_features // self.num_heads
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def scale(self) -> float:
|
|
160
|
+
"""Dot-product attention scaling factor."""
|
|
161
|
+
return self.head_dim**-0.5
|
|
162
|
+
|
|
163
|
+
def __call__(
|
|
164
|
+
self,
|
|
165
|
+
node_features: Float[Array, "n_nodes in_features"],
|
|
166
|
+
edge_index: Int[Array, "2 n_edges"],
|
|
167
|
+
edge_features: Float[Array, "n_edges edge_features"],
|
|
168
|
+
*,
|
|
169
|
+
deterministic: bool = True,
|
|
170
|
+
) -> Float[Array, "n_nodes out_features"]:
|
|
171
|
+
"""Run one graph-attention update step.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
node_features: Node feature matrix of shape ``(n_nodes, in_features)``.
|
|
175
|
+
edge_index: Edge indices ``(source, target)`` of shape ``(2, n_edges)``.
|
|
176
|
+
edge_features: Edge feature matrix of shape ``(n_edges, edge_features)``.
|
|
177
|
+
deterministic: Whether to disable stochastic dropout.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Updated node features of shape ``(n_nodes, out_features)``.
|
|
181
|
+
"""
|
|
182
|
+
n_nodes = node_features.shape[0]
|
|
183
|
+
n_edges = edge_index.shape[1]
|
|
184
|
+
out_features = self.num_heads * self.head_dim
|
|
185
|
+
|
|
186
|
+
# Handle empty graph: no edges means no messages to aggregate
|
|
187
|
+
if n_edges == 0:
|
|
188
|
+
return self.output_proj(jnp.zeros((n_nodes, out_features)))
|
|
189
|
+
|
|
190
|
+
sources = edge_index[0] # (n_edges,)
|
|
191
|
+
targets = edge_index[1] # (n_edges,)
|
|
192
|
+
|
|
193
|
+
# Project all nodes
|
|
194
|
+
queries = self.query_proj(node_features) # (n_nodes, out_features)
|
|
195
|
+
keys = self.key_proj(node_features)
|
|
196
|
+
values = self.value_proj(node_features)
|
|
197
|
+
|
|
198
|
+
# Reshape for multi-head attention
|
|
199
|
+
queries = queries.reshape(n_nodes, self.num_heads, self.head_dim)
|
|
200
|
+
keys = keys.reshape(n_nodes, self.num_heads, self.head_dim)
|
|
201
|
+
values = values.reshape(n_nodes, self.num_heads, self.head_dim)
|
|
202
|
+
|
|
203
|
+
# Get source and target features for each edge
|
|
204
|
+
query_targets = queries[targets] # (n_edges, num_heads, head_dim)
|
|
205
|
+
key_sources = keys[sources] # (n_edges, num_heads, head_dim)
|
|
206
|
+
value_sources = values[sources] # (n_edges, num_heads, head_dim)
|
|
207
|
+
|
|
208
|
+
# Compute attention scores
|
|
209
|
+
attn_scores = (
|
|
210
|
+
jnp.sum(query_targets * key_sources, axis=-1) * self.scale
|
|
211
|
+
) # (n_edges, num_heads)
|
|
212
|
+
|
|
213
|
+
# Add edge feature bias
|
|
214
|
+
edge_bias = self.edge_proj(edge_features) # (n_edges, num_heads)
|
|
215
|
+
attn_scores = attn_scores + edge_bias
|
|
216
|
+
|
|
217
|
+
# Normalize attention per target node using segment_max/segment_sum
|
|
218
|
+
# This is equivalent to softmax over incoming edges per node
|
|
219
|
+
max_scores = jax.ops.segment_max(
|
|
220
|
+
attn_scores, targets, num_segments=n_nodes
|
|
221
|
+
) # (n_nodes, num_heads)
|
|
222
|
+
attn_scores = attn_scores - max_scores[targets] # Stability
|
|
223
|
+
attn_exp = jnp.exp(attn_scores)
|
|
224
|
+
|
|
225
|
+
# Sum of exp scores per target node
|
|
226
|
+
attn_sum = (
|
|
227
|
+
jax.ops.segment_sum(attn_exp, targets, num_segments=n_nodes) + 1e-10
|
|
228
|
+
) # (n_nodes, num_heads)
|
|
229
|
+
|
|
230
|
+
# Normalize
|
|
231
|
+
attn_probs = attn_exp / attn_sum[targets] # (n_edges, num_heads)
|
|
232
|
+
|
|
233
|
+
# Apply dropout
|
|
234
|
+
if self.dropout is not None and not deterministic:
|
|
235
|
+
attn_probs = self.dropout(attn_probs)
|
|
236
|
+
|
|
237
|
+
# Weighted sum of values
|
|
238
|
+
weighted_values = attn_probs[:, :, None] * value_sources # (n_edges, num_heads, head_dim)
|
|
239
|
+
|
|
240
|
+
# Aggregate to target nodes
|
|
241
|
+
aggregated = jax.ops.segment_sum(
|
|
242
|
+
weighted_values.reshape(n_edges, -1), targets, num_segments=n_nodes
|
|
243
|
+
) # (n_nodes, out_features)
|
|
244
|
+
|
|
245
|
+
# Output projection
|
|
246
|
+
return self.output_proj(aggregated)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class GraphAttentionBlock(nnx.Module):
|
|
250
|
+
"""Full GNN block: graph attention + LayerNorm + residual + feedforward.
|
|
251
|
+
|
|
252
|
+
Combines a :class:`GraphAttentionLayer` with pre-norm residual connections
|
|
253
|
+
and a two-layer feedforward network (4x expansion), following the
|
|
254
|
+
standard Transformer block pattern adapted for graphs.
|
|
255
|
+
|
|
256
|
+
Architecture::
|
|
257
|
+
|
|
258
|
+
x -> GraphAttentionLayer -> + -> LayerNorm -> FFN -> + -> LayerNorm -> out
|
|
259
|
+
|___________________________| |_________|
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
hidden_dim: Hidden dimension (both input and output).
|
|
263
|
+
num_heads: Number of attention heads.
|
|
264
|
+
edge_features: Edge feature dimension.
|
|
265
|
+
dropout_rate: Dropout rate.
|
|
266
|
+
rngs: Flax NNX random number generators.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def __init__(
|
|
270
|
+
self,
|
|
271
|
+
hidden_dim: int,
|
|
272
|
+
num_heads: int,
|
|
273
|
+
edge_features: int,
|
|
274
|
+
dropout_rate: float,
|
|
275
|
+
*,
|
|
276
|
+
rngs: nnx.Rngs,
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Initialize the GNN block.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
hidden_dim: Hidden dimension.
|
|
282
|
+
num_heads: Number of attention heads.
|
|
283
|
+
edge_features: Edge feature dimension.
|
|
284
|
+
dropout_rate: Dropout rate.
|
|
285
|
+
rngs: Random number generators.
|
|
286
|
+
"""
|
|
287
|
+
super().__init__()
|
|
288
|
+
|
|
289
|
+
self.attention = GraphAttentionLayer(
|
|
290
|
+
in_features=hidden_dim,
|
|
291
|
+
out_features=hidden_dim,
|
|
292
|
+
num_heads=num_heads,
|
|
293
|
+
edge_features=edge_features,
|
|
294
|
+
dropout_rate=dropout_rate,
|
|
295
|
+
rngs=rngs,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
self.layer_norm1 = nnx.LayerNorm(num_features=hidden_dim, rngs=rngs)
|
|
299
|
+
self.layer_norm2 = nnx.LayerNorm(num_features=hidden_dim, rngs=rngs)
|
|
300
|
+
|
|
301
|
+
# Feedforward with 4x expansion
|
|
302
|
+
self.ff_linear1 = nnx.Linear(
|
|
303
|
+
in_features=hidden_dim,
|
|
304
|
+
out_features=hidden_dim * 4,
|
|
305
|
+
rngs=rngs,
|
|
306
|
+
)
|
|
307
|
+
self.ff_linear2 = nnx.Linear(
|
|
308
|
+
in_features=hidden_dim * 4,
|
|
309
|
+
out_features=hidden_dim,
|
|
310
|
+
rngs=rngs,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def __call__(
|
|
314
|
+
self,
|
|
315
|
+
node_features: Float[Array, "n_nodes hidden_dim"],
|
|
316
|
+
edge_index: Int[Array, "2 n_edges"],
|
|
317
|
+
edge_features: Float[Array, "n_edges edge_features"],
|
|
318
|
+
*,
|
|
319
|
+
deterministic: bool = True,
|
|
320
|
+
) -> Float[Array, "n_nodes hidden_dim"]:
|
|
321
|
+
"""Apply the GNN block.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
node_features: Node features of shape ``(n_nodes, hidden_dim)``.
|
|
325
|
+
edge_index: Edge indices of shape ``(2, n_edges)``.
|
|
326
|
+
edge_features: Edge features of shape ``(n_edges, edge_features)``.
|
|
327
|
+
deterministic: If True, disable dropout.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Updated node features of shape ``(n_nodes, hidden_dim)``.
|
|
331
|
+
"""
|
|
332
|
+
# Attention with residual
|
|
333
|
+
attended = self.attention(
|
|
334
|
+
node_features, edge_index, edge_features, deterministic=deterministic
|
|
335
|
+
)
|
|
336
|
+
x = self.layer_norm1(node_features + attended)
|
|
337
|
+
|
|
338
|
+
# Feedforward with residual
|
|
339
|
+
ff_out = self.ff_linear2(nnx.gelu(self.ff_linear1(x)))
|
|
340
|
+
x = self.layer_norm2(x + ff_out)
|
|
341
|
+
|
|
342
|
+
return x
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class GATv2Layer(nnx.Module):
|
|
346
|
+
"""GATv2 multi-head graph attention layer.
|
|
347
|
+
|
|
348
|
+
Unlike the original GAT (``GraphAttentionLayer``), GATv2 applies LeakyReLU
|
|
349
|
+
*before* computing the attention scalar, which makes the attention function
|
|
350
|
+
strictly more expressive (it can represent any monotonic scoring function
|
|
351
|
+
over concatenated source/target features).
|
|
352
|
+
|
|
353
|
+
GATv2 attention::
|
|
354
|
+
|
|
355
|
+
e_{ij} = a^T * LeakyReLU(W_l * h_i + W_r * h_j + edge_bias)
|
|
356
|
+
|
|
357
|
+
This is the key difference from GAT, where the nonlinearity is applied
|
|
358
|
+
*after* the attention dot product.
|
|
359
|
+
|
|
360
|
+
Reference: Brody, Alon, Yahav. "How Attentive are Graph Attention
|
|
361
|
+
Networks?" (ICLR 2022).
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
in_features: Input node feature dimension.
|
|
365
|
+
out_features: Output feature dimension (must be divisible by num_heads).
|
|
366
|
+
num_heads: Number of parallel attention heads.
|
|
367
|
+
edge_features: Edge feature dimension.
|
|
368
|
+
dropout_rate: Dropout rate applied to attention weights.
|
|
369
|
+
negative_slope: Negative slope for LeakyReLU (default 0.2).
|
|
370
|
+
rngs: Flax NNX random number generators.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
def __init__(
|
|
374
|
+
self,
|
|
375
|
+
in_features: int,
|
|
376
|
+
out_features: int,
|
|
377
|
+
num_heads: int,
|
|
378
|
+
edge_features: int,
|
|
379
|
+
dropout_rate: float,
|
|
380
|
+
negative_slope: float = 0.2,
|
|
381
|
+
*,
|
|
382
|
+
rngs: nnx.Rngs,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Initialize the GATv2 layer.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
in_features: Input feature dimension.
|
|
388
|
+
out_features: Output feature dimension.
|
|
389
|
+
num_heads: Number of attention heads.
|
|
390
|
+
edge_features: Edge feature dimension.
|
|
391
|
+
dropout_rate: Dropout rate.
|
|
392
|
+
negative_slope: Negative slope for LeakyReLU.
|
|
393
|
+
rngs: Random number generators.
|
|
394
|
+
"""
|
|
395
|
+
super().__init__()
|
|
396
|
+
if num_heads <= 0:
|
|
397
|
+
raise ValueError("num_heads must be positive")
|
|
398
|
+
if out_features <= 0:
|
|
399
|
+
raise ValueError("out_features must be positive")
|
|
400
|
+
if out_features % num_heads != 0:
|
|
401
|
+
raise ValueError("out_features must be divisible by num_heads")
|
|
402
|
+
if edge_features <= 0:
|
|
403
|
+
raise ValueError("edge_features must be positive")
|
|
404
|
+
if negative_slope < 0.0:
|
|
405
|
+
raise ValueError("negative_slope must be non-negative")
|
|
406
|
+
if not 0.0 <= dropout_rate < 1.0:
|
|
407
|
+
raise ValueError("dropout_rate must be in [0, 1)")
|
|
408
|
+
|
|
409
|
+
# GATv2 uses separate left/right projections (not Q/K/V)
|
|
410
|
+
self.attn_projections = _BidirectionalProjection(
|
|
411
|
+
in_features=in_features,
|
|
412
|
+
out_features=out_features,
|
|
413
|
+
rngs=rngs,
|
|
414
|
+
negative_slope=negative_slope,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Edge feature projection to per-head bias
|
|
418
|
+
self.edge_proj = nnx.Linear(
|
|
419
|
+
in_features=edge_features,
|
|
420
|
+
out_features=num_heads,
|
|
421
|
+
rngs=rngs,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Per-head attention vector a^T (applied after LeakyReLU)
|
|
425
|
+
self.attn_vector = nnx.Param(
|
|
426
|
+
jax.random.normal(rngs.params(), (num_heads, self.head_dim)) * 0.01
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Value projection and output projection
|
|
430
|
+
self.value_proj = nnx.Linear(
|
|
431
|
+
in_features=in_features,
|
|
432
|
+
out_features=out_features,
|
|
433
|
+
rngs=rngs,
|
|
434
|
+
)
|
|
435
|
+
self.output_proj = nnx.Linear(
|
|
436
|
+
in_features=out_features,
|
|
437
|
+
out_features=out_features,
|
|
438
|
+
rngs=rngs,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
self.dropout = nnx.Dropout(rate=dropout_rate, rngs=rngs) if dropout_rate > 0 else None
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
def num_heads(self) -> int:
|
|
445
|
+
"""Number of attention heads."""
|
|
446
|
+
return self.edge_proj.out_features
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def head_dim(self) -> int:
|
|
450
|
+
"""Per-head hidden dimension."""
|
|
451
|
+
return self.attn_projections.left.out_features // self.num_heads
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def negative_slope(self) -> float:
|
|
455
|
+
"""Negative slope used by the LeakyReLU attention scorer."""
|
|
456
|
+
if self.attn_projections.negative_slope is None:
|
|
457
|
+
raise ValueError("negative_slope is not configured")
|
|
458
|
+
return self.attn_projections.negative_slope
|
|
459
|
+
|
|
460
|
+
def __call__(
|
|
461
|
+
self,
|
|
462
|
+
node_features: Float[Array, "n_nodes in_features"],
|
|
463
|
+
edge_index: Int[Array, "2 n_edges"],
|
|
464
|
+
edge_features: Float[Array, "n_edges edge_features"],
|
|
465
|
+
*,
|
|
466
|
+
deterministic: bool = True,
|
|
467
|
+
) -> Float[Array, "n_nodes out_features"]:
|
|
468
|
+
"""Run one GATv2 attention update step.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
node_features: Node feature matrix ``(n_nodes, in_features)``.
|
|
472
|
+
edge_index: Edge indices ``(source, target)`` of shape ``(2, n_edges)``.
|
|
473
|
+
edge_features: Edge feature matrix ``(n_edges, edge_features)``.
|
|
474
|
+
deterministic: Whether to disable stochastic dropout.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Updated node features of shape ``(n_nodes, out_features)``.
|
|
478
|
+
"""
|
|
479
|
+
n_nodes = node_features.shape[0]
|
|
480
|
+
n_edges = edge_index.shape[1]
|
|
481
|
+
out_features = self.num_heads * self.head_dim
|
|
482
|
+
|
|
483
|
+
# Handle empty graph
|
|
484
|
+
if n_edges == 0:
|
|
485
|
+
return self.output_proj(jnp.zeros((n_nodes, out_features)))
|
|
486
|
+
|
|
487
|
+
sources = edge_index[0]
|
|
488
|
+
targets = edge_index[1]
|
|
489
|
+
|
|
490
|
+
# Left/right projections for all nodes
|
|
491
|
+
left, right = self.attn_projections(node_features)
|
|
492
|
+
|
|
493
|
+
# Reshape to per-head: (n_nodes, num_heads, head_dim)
|
|
494
|
+
left = left.reshape(n_nodes, self.num_heads, self.head_dim)
|
|
495
|
+
right = right.reshape(n_nodes, self.num_heads, self.head_dim)
|
|
496
|
+
|
|
497
|
+
# Gather per-edge: left for targets, right for sources
|
|
498
|
+
left_targets = left[targets] # (n_edges, num_heads, head_dim)
|
|
499
|
+
right_sources = right[sources]
|
|
500
|
+
|
|
501
|
+
# GATv2 key: LeakyReLU BEFORE the attention dot product
|
|
502
|
+
combined = left_targets + right_sources # (n_edges, num_heads, head_dim)
|
|
503
|
+
activated = jax.nn.leaky_relu(combined, negative_slope=self.negative_slope)
|
|
504
|
+
|
|
505
|
+
# Attention score: a^T * activated => (n_edges, num_heads)
|
|
506
|
+
attn_scores = jnp.sum(activated * self.attn_vector[...][None, :, :], axis=-1)
|
|
507
|
+
|
|
508
|
+
# Add edge feature bias
|
|
509
|
+
edge_bias = self.edge_proj(edge_features) # (n_edges, num_heads)
|
|
510
|
+
attn_scores = attn_scores + edge_bias
|
|
511
|
+
|
|
512
|
+
# Segment-softmax normalization over incoming edges per target node
|
|
513
|
+
max_scores = jax.ops.segment_max(attn_scores, targets, num_segments=n_nodes)
|
|
514
|
+
attn_scores = attn_scores - max_scores[targets]
|
|
515
|
+
attn_exp = jnp.exp(attn_scores)
|
|
516
|
+
|
|
517
|
+
attn_sum = jax.ops.segment_sum(attn_exp, targets, num_segments=n_nodes) + 1e-10
|
|
518
|
+
attn_probs = attn_exp / attn_sum[targets] # (n_edges, num_heads)
|
|
519
|
+
|
|
520
|
+
# Apply dropout
|
|
521
|
+
if self.dropout is not None and not deterministic:
|
|
522
|
+
attn_probs = self.dropout(attn_probs)
|
|
523
|
+
|
|
524
|
+
# Value aggregation
|
|
525
|
+
values = self.value_proj(node_features).reshape(n_nodes, self.num_heads, self.head_dim)
|
|
526
|
+
value_sources = values[sources] # (n_edges, num_heads, head_dim)
|
|
527
|
+
weighted_values = attn_probs[:, :, None] * value_sources
|
|
528
|
+
|
|
529
|
+
aggregated = jax.ops.segment_sum(
|
|
530
|
+
weighted_values.reshape(n_edges, -1), targets, num_segments=n_nodes
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return self.output_proj(aggregated)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class GATv2Block(nnx.Module):
|
|
537
|
+
"""Full GNN block using GATv2 attention + LayerNorm + residual + FFN.
|
|
538
|
+
|
|
539
|
+
Architecture::
|
|
540
|
+
|
|
541
|
+
x -> GATv2Layer -> + -> LayerNorm -> FFN -> + -> LayerNorm -> out
|
|
542
|
+
|__________________| |_________|
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
hidden_dim: Hidden dimension (both input and output).
|
|
546
|
+
num_heads: Number of attention heads.
|
|
547
|
+
edge_features: Edge feature dimension.
|
|
548
|
+
dropout_rate: Dropout rate.
|
|
549
|
+
negative_slope: Negative slope for LeakyReLU in GATv2 attention.
|
|
550
|
+
rngs: Flax NNX random number generators.
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
def __init__(
|
|
554
|
+
self,
|
|
555
|
+
hidden_dim: int,
|
|
556
|
+
num_heads: int,
|
|
557
|
+
edge_features: int,
|
|
558
|
+
dropout_rate: float,
|
|
559
|
+
negative_slope: float = 0.2,
|
|
560
|
+
*,
|
|
561
|
+
rngs: nnx.Rngs,
|
|
562
|
+
) -> None:
|
|
563
|
+
"""Initialize the GATv2 block.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
hidden_dim: Hidden dimension.
|
|
567
|
+
num_heads: Number of attention heads.
|
|
568
|
+
edge_features: Edge feature dimension.
|
|
569
|
+
dropout_rate: Dropout rate.
|
|
570
|
+
negative_slope: Negative slope for LeakyReLU.
|
|
571
|
+
rngs: Random number generators.
|
|
572
|
+
"""
|
|
573
|
+
super().__init__()
|
|
574
|
+
|
|
575
|
+
self.attention = GATv2Layer(
|
|
576
|
+
in_features=hidden_dim,
|
|
577
|
+
out_features=hidden_dim,
|
|
578
|
+
num_heads=num_heads,
|
|
579
|
+
edge_features=edge_features,
|
|
580
|
+
dropout_rate=dropout_rate,
|
|
581
|
+
negative_slope=negative_slope,
|
|
582
|
+
rngs=rngs,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
self.layer_norm1 = nnx.LayerNorm(num_features=hidden_dim, rngs=rngs)
|
|
586
|
+
self.layer_norm2 = nnx.LayerNorm(num_features=hidden_dim, rngs=rngs)
|
|
587
|
+
|
|
588
|
+
# Feedforward with 4x expansion
|
|
589
|
+
self.ff_linear1 = nnx.Linear(
|
|
590
|
+
in_features=hidden_dim,
|
|
591
|
+
out_features=hidden_dim * 4,
|
|
592
|
+
rngs=rngs,
|
|
593
|
+
)
|
|
594
|
+
self.ff_linear2 = nnx.Linear(
|
|
595
|
+
in_features=hidden_dim * 4,
|
|
596
|
+
out_features=hidden_dim,
|
|
597
|
+
rngs=rngs,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
def __call__(
|
|
601
|
+
self,
|
|
602
|
+
node_features: Float[Array, "n_nodes hidden_dim"],
|
|
603
|
+
edge_index: Int[Array, "2 n_edges"],
|
|
604
|
+
edge_features: Float[Array, "n_edges edge_features"],
|
|
605
|
+
*,
|
|
606
|
+
deterministic: bool = True,
|
|
607
|
+
) -> Float[Array, "n_nodes hidden_dim"]:
|
|
608
|
+
"""Apply the GATv2 block.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
node_features: Node features ``(n_nodes, hidden_dim)``.
|
|
612
|
+
edge_index: Edge indices ``(2, n_edges)``.
|
|
613
|
+
edge_features: Edge features ``(n_edges, edge_features)``.
|
|
614
|
+
deterministic: If True, disable dropout.
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
Updated node features ``(n_nodes, hidden_dim)``.
|
|
618
|
+
"""
|
|
619
|
+
# Attention with residual
|
|
620
|
+
attended = self.attention(
|
|
621
|
+
node_features, edge_index, edge_features, deterministic=deterministic
|
|
622
|
+
)
|
|
623
|
+
x = self.layer_norm1(node_features + attended)
|
|
624
|
+
|
|
625
|
+
# Feedforward with residual
|
|
626
|
+
ff_out = self.ff_linear2(nnx.gelu(self.ff_linear1(x)))
|
|
627
|
+
x = self.layer_norm2(x + ff_out)
|
|
628
|
+
|
|
629
|
+
return x
|