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,267 @@
|
|
|
1
|
+
"""Differentiable MACCS structural keys fingerprint operator.
|
|
2
|
+
|
|
3
|
+
This module implements a differentiable version of the 166 MACCS
|
|
4
|
+
(Molecular ACCess System) structural keys fingerprint.
|
|
5
|
+
|
|
6
|
+
MACCS keys are predefined structural patterns (SMARTS) that encode
|
|
7
|
+
the presence/absence of specific molecular substructures. This
|
|
8
|
+
implementation provides a differentiable approximation using
|
|
9
|
+
learned pattern matching networks.
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
- https://rdkit.org/docs/source/rdkit.Chem.MACCSkeys.html
|
|
13
|
+
- Durant et al. "Reoptimization of MDL Keys for Use in Drug Discovery" JCIM 2002
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import jax.numpy as jnp
|
|
21
|
+
from datarax.core.config import OperatorConfig
|
|
22
|
+
from datarax.core.operator import OperatorModule
|
|
23
|
+
from flax import nnx
|
|
24
|
+
|
|
25
|
+
from diffbio.core import soft_ops
|
|
26
|
+
from diffbio.operators.drug_discovery._graph_utils import (
|
|
27
|
+
attach_fingerprint,
|
|
28
|
+
ensure_rngs,
|
|
29
|
+
initialize_graph_encoder,
|
|
30
|
+
stabilize_operator_id,
|
|
31
|
+
unpack_graph_inputs,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class MACCSKeysConfig(OperatorConfig):
|
|
39
|
+
"""Configuration for MACCS keys fingerprint operator.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
n_bits: Number of fingerprint bits (default: 166 for standard MACCS).
|
|
43
|
+
differentiable: Use learned pattern matching (default: True).
|
|
44
|
+
temperature: Temperature for soft bit assignment (default: 1.0).
|
|
45
|
+
hidden_dim: Hidden dimension for pattern networks (default: 64).
|
|
46
|
+
num_layers: Number of message passing layers (default: 2).
|
|
47
|
+
in_features: Number of input node features (default: 4).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
n_bits: int = 166
|
|
51
|
+
differentiable: bool = True
|
|
52
|
+
temperature: float = 1.0
|
|
53
|
+
hidden_dim: int = 64
|
|
54
|
+
num_layers: int = 2
|
|
55
|
+
in_features: int = 4
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MACCSKeysOperator(OperatorModule):
|
|
59
|
+
"""Differentiable MACCS structural keys fingerprint operator.
|
|
60
|
+
|
|
61
|
+
For differentiable=True:
|
|
62
|
+
Uses message passing and learned pattern detectors to approximate
|
|
63
|
+
MACCS key detection. Each of the 166 keys is represented by a
|
|
64
|
+
learned pattern matching network that outputs a soft presence score.
|
|
65
|
+
|
|
66
|
+
For differentiable=False:
|
|
67
|
+
Would use RDKit's exact MACCS implementation (not differentiable).
|
|
68
|
+
|
|
69
|
+
The differentiable version enables gradient flow for end-to-end
|
|
70
|
+
optimization while approximating the structural pattern detection
|
|
71
|
+
of traditional MACCS keys.
|
|
72
|
+
|
|
73
|
+
MACCS keys encode various structural features:
|
|
74
|
+
|
|
75
|
+
- Atom types (C, N, O, S, halides, etc.)
|
|
76
|
+
- Functional groups (carbonyl, hydroxyl, amine, etc.)
|
|
77
|
+
- Ring systems (aromatic, aliphatic)
|
|
78
|
+
- Bond patterns and connectivity
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
```python
|
|
82
|
+
config = MACCSKeysConfig(temperature=1.0)
|
|
83
|
+
op = MACCSKeysOperator(config, rngs=nnx.Rngs(42))
|
|
84
|
+
data = {"node_features": nodes, "adjacency": adj}
|
|
85
|
+
result, _, _ = op.apply(data, {}, None)
|
|
86
|
+
fingerprint = result["fingerprint"] # shape: (166,)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
References:
|
|
90
|
+
- Durant et al. "Reoptimization of MDL Keys" JCIM 2002
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
config: MACCSKeysConfig,
|
|
96
|
+
*,
|
|
97
|
+
rngs: nnx.Rngs | None = None,
|
|
98
|
+
):
|
|
99
|
+
"""Initialize MACCS keys operator.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
config: MACCS keys configuration.
|
|
103
|
+
rngs: Flax NNX random number generators.
|
|
104
|
+
"""
|
|
105
|
+
super().__init__(config, rngs=rngs)
|
|
106
|
+
|
|
107
|
+
rngs = ensure_rngs(rngs)
|
|
108
|
+
|
|
109
|
+
if config.differentiable:
|
|
110
|
+
# Message passing for local structure aggregation
|
|
111
|
+
rngs = initialize_graph_encoder(
|
|
112
|
+
self,
|
|
113
|
+
rngs=rngs,
|
|
114
|
+
hidden_dim=config.hidden_dim,
|
|
115
|
+
num_layers=config.num_layers,
|
|
116
|
+
in_features=config.in_features,
|
|
117
|
+
attr="encoder",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Pattern detectors: one network per MACCS key
|
|
121
|
+
# Each outputs a score indicating pattern presence
|
|
122
|
+
self.pattern_detectors = nnx.Sequential(
|
|
123
|
+
nnx.Linear(config.hidden_dim, config.hidden_dim, rngs=rngs),
|
|
124
|
+
nnx.relu,
|
|
125
|
+
nnx.Linear(config.hidden_dim, config.n_bits, rngs=rngs),
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
# RDKit mode
|
|
129
|
+
stabilize_operator_id(self)
|
|
130
|
+
try:
|
|
131
|
+
from rdkit import Chem
|
|
132
|
+
from rdkit.Chem import MACCSkeys as RDKitMACCS
|
|
133
|
+
|
|
134
|
+
self._Chem = Chem
|
|
135
|
+
self._MACCSkeys = RDKitMACCS
|
|
136
|
+
except ImportError as e:
|
|
137
|
+
raise ImportError(
|
|
138
|
+
"MACCSKeysOperator with differentiable=False requires RDKit: pip install rdkit"
|
|
139
|
+
) from e
|
|
140
|
+
|
|
141
|
+
def _compute_differentiable_fp(
|
|
142
|
+
self,
|
|
143
|
+
node_features: jnp.ndarray,
|
|
144
|
+
adjacency: jnp.ndarray,
|
|
145
|
+
edge_features: jnp.ndarray | None = None,
|
|
146
|
+
node_mask: jnp.ndarray | None = None,
|
|
147
|
+
) -> jnp.ndarray:
|
|
148
|
+
"""Compute differentiable MACCS-style fingerprint.
|
|
149
|
+
|
|
150
|
+
Uses message passing to aggregate local structure information,
|
|
151
|
+
then applies learned pattern detectors with soft thresholding.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
node_features: (num_nodes, num_features) atom features
|
|
155
|
+
adjacency: (num_nodes, num_nodes) adjacency matrix
|
|
156
|
+
edge_features: Optional edge features
|
|
157
|
+
node_mask: Optional mask for valid nodes
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Fingerprint vector of shape (n_bits,)
|
|
161
|
+
"""
|
|
162
|
+
# Message passing to capture local structure
|
|
163
|
+
node_hidden = self.encoder(node_features, adjacency, edge_features)
|
|
164
|
+
|
|
165
|
+
# Apply node mask
|
|
166
|
+
if node_mask is not None:
|
|
167
|
+
node_hidden = node_hidden * node_mask[:, None]
|
|
168
|
+
|
|
169
|
+
# Pattern detection at each atom
|
|
170
|
+
# pattern_logits: (num_nodes, n_bits)
|
|
171
|
+
pattern_logits = self.pattern_detectors(node_hidden)
|
|
172
|
+
|
|
173
|
+
# Apply temperature-scaled sigmoid for soft pattern matching
|
|
174
|
+
soft_patterns = soft_ops.greater(pattern_logits, 0.0, softness=self.config.temperature)
|
|
175
|
+
|
|
176
|
+
# Aggregate across atoms using max (OR-like)
|
|
177
|
+
# If any atom matches a pattern, the bit is set
|
|
178
|
+
fingerprint = soft_ops.max(soft_patterns, axis=0, softness=self.config.temperature)
|
|
179
|
+
|
|
180
|
+
return fingerprint
|
|
181
|
+
|
|
182
|
+
def apply(
|
|
183
|
+
self,
|
|
184
|
+
data: dict[str, Any],
|
|
185
|
+
state: dict[str, Any],
|
|
186
|
+
metadata: dict[str, Any] | None,
|
|
187
|
+
random_params: Any = None, # noqa: ARG002
|
|
188
|
+
stats: dict[str, Any] | None = None, # noqa: ARG002
|
|
189
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]:
|
|
190
|
+
"""Compute MACCS keys fingerprint.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
data: Input data containing:
|
|
194
|
+
For differentiable=True:
|
|
195
|
+
- node_features: (num_nodes, num_features) atom features
|
|
196
|
+
- adjacency: (num_nodes, num_nodes) adjacency matrix
|
|
197
|
+
- node_mask: (num_nodes,) optional mask for valid nodes
|
|
198
|
+
For differentiable=False:
|
|
199
|
+
- smiles: SMILES string
|
|
200
|
+
state: Per-element state (passed through).
|
|
201
|
+
metadata: Optional metadata.
|
|
202
|
+
random_params: Unused random parameters.
|
|
203
|
+
stats: Optional statistics dictionary.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Tuple of:
|
|
207
|
+
- data with added "fingerprint" key
|
|
208
|
+
- unchanged state
|
|
209
|
+
- unchanged metadata
|
|
210
|
+
"""
|
|
211
|
+
del random_params, stats # Unused
|
|
212
|
+
|
|
213
|
+
if self.config.differentiable:
|
|
214
|
+
node_features, adjacency, edge_features, node_mask = unpack_graph_inputs(data)
|
|
215
|
+
fp = self._compute_differentiable_fp(node_features, adjacency, edge_features, node_mask)
|
|
216
|
+
else:
|
|
217
|
+
smiles = data["smiles"]
|
|
218
|
+
fp = self._compute_rdkit_fp(smiles)
|
|
219
|
+
|
|
220
|
+
return attach_fingerprint(data, fp), state, metadata
|
|
221
|
+
|
|
222
|
+
def _compute_rdkit_fp(self, smiles: str) -> jnp.ndarray:
|
|
223
|
+
"""Compute exact MACCS keys using RDKit.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
smiles: SMILES string
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Binary fingerprint vector of shape (166,)
|
|
230
|
+
"""
|
|
231
|
+
import numpy as np
|
|
232
|
+
|
|
233
|
+
mol = self._Chem.MolFromSmiles(smiles)
|
|
234
|
+
if mol is None:
|
|
235
|
+
return jnp.zeros(self.config.n_bits, dtype=jnp.float32)
|
|
236
|
+
|
|
237
|
+
fp = self._MACCSkeys.GenMACCSKeys(mol)
|
|
238
|
+
|
|
239
|
+
# Convert to numpy (RDKit returns 167 bits, we use 1-166)
|
|
240
|
+
arr = np.zeros(167, dtype=np.float32)
|
|
241
|
+
for i in range(167):
|
|
242
|
+
arr[i] = fp.GetBit(i)
|
|
243
|
+
|
|
244
|
+
# Return bits 1-166 (index 0 is unused in standard MACCS)
|
|
245
|
+
return jnp.asarray(arr[1:167])
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def create_maccs_operator(
|
|
249
|
+
differentiable: bool = True,
|
|
250
|
+
temperature: float = 1.0,
|
|
251
|
+
seed: int = 42,
|
|
252
|
+
) -> MACCSKeysOperator:
|
|
253
|
+
"""Create a MACCS keys fingerprint operator.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
differentiable: Use learned pattern matching.
|
|
257
|
+
temperature: Temperature for soft matching.
|
|
258
|
+
seed: Random seed.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Configured MACCSKeysOperator.
|
|
262
|
+
"""
|
|
263
|
+
config = MACCSKeysConfig(
|
|
264
|
+
differentiable=differentiable,
|
|
265
|
+
temperature=temperature,
|
|
266
|
+
)
|
|
267
|
+
return MACCSKeysOperator(config, rngs=nnx.Rngs(seed))
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Message passing neural network layers for molecular graphs.
|
|
2
|
+
|
|
3
|
+
This module implements directed message passing neural network (D-MPNN)
|
|
4
|
+
layers following the ChemProp architecture for molecular property prediction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
import jax.numpy as jnp
|
|
10
|
+
from flax import nnx
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MessagePassingLayer(nnx.Module):
|
|
16
|
+
"""Directed message passing layer for molecular graphs.
|
|
17
|
+
|
|
18
|
+
Implements the D-MPNN message passing scheme where messages are passed
|
|
19
|
+
along directed edges. Each node aggregates messages from its neighbors
|
|
20
|
+
and updates its representation.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
hidden_dim: Dimension of hidden node representations.
|
|
24
|
+
in_features: Number of input node features.
|
|
25
|
+
num_edge_features: Number of edge features (default 4 for bond types).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
hidden_dim: int,
|
|
31
|
+
in_features: int = 4,
|
|
32
|
+
num_edge_features: int = 4,
|
|
33
|
+
*,
|
|
34
|
+
rngs: nnx.Rngs,
|
|
35
|
+
):
|
|
36
|
+
"""Initialize message passing layer.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
hidden_dim: Dimension of hidden representations.
|
|
40
|
+
in_features: Number of input node features (default 4 for tests).
|
|
41
|
+
num_edge_features: Number of edge/bond features.
|
|
42
|
+
rngs: Flax NNX random number generators.
|
|
43
|
+
"""
|
|
44
|
+
self.hidden_dim = hidden_dim
|
|
45
|
+
self.in_features = in_features
|
|
46
|
+
self.num_edge_features = num_edge_features
|
|
47
|
+
|
|
48
|
+
# Node encoder - eagerly initialized with specified in_features
|
|
49
|
+
self.node_encoder = nnx.Linear(
|
|
50
|
+
in_features=in_features,
|
|
51
|
+
out_features=hidden_dim,
|
|
52
|
+
rngs=rngs,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Edge encoder
|
|
56
|
+
self.edge_encoder = nnx.Linear(
|
|
57
|
+
in_features=num_edge_features,
|
|
58
|
+
out_features=hidden_dim,
|
|
59
|
+
rngs=rngs,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Message transformation
|
|
63
|
+
self.message_layer = nnx.Linear(
|
|
64
|
+
in_features=hidden_dim * 3, # src_node + edge + dst_node
|
|
65
|
+
out_features=hidden_dim,
|
|
66
|
+
rngs=rngs,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Update function (GRU-like update)
|
|
70
|
+
self.update_layer = nnx.Linear(
|
|
71
|
+
in_features=hidden_dim * 2, # current + aggregated
|
|
72
|
+
out_features=hidden_dim,
|
|
73
|
+
rngs=rngs,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __call__(
|
|
77
|
+
self,
|
|
78
|
+
node_features: jnp.ndarray,
|
|
79
|
+
adjacency: jnp.ndarray,
|
|
80
|
+
edge_features: jnp.ndarray | None = None,
|
|
81
|
+
) -> jnp.ndarray:
|
|
82
|
+
"""Perform one step of message passing.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
node_features: Node features of shape (num_nodes, in_features).
|
|
86
|
+
adjacency: Adjacency matrix of shape (num_nodes, num_nodes).
|
|
87
|
+
edge_features: Optional edge features of shape
|
|
88
|
+
(num_nodes, num_nodes, num_edge_features).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Updated node features of shape (num_nodes, hidden_dim).
|
|
92
|
+
"""
|
|
93
|
+
num_nodes = node_features.shape[0]
|
|
94
|
+
|
|
95
|
+
# Encode node features to hidden dimension
|
|
96
|
+
node_hidden = nnx.relu(self.node_encoder(node_features))
|
|
97
|
+
|
|
98
|
+
# Handle edge features
|
|
99
|
+
if edge_features is not None:
|
|
100
|
+
edge_hidden = nnx.relu(self.edge_encoder(edge_features))
|
|
101
|
+
else:
|
|
102
|
+
# Use zeros if no edge features provided
|
|
103
|
+
edge_hidden = jnp.zeros((num_nodes, num_nodes, self.hidden_dim), dtype=jnp.float32)
|
|
104
|
+
|
|
105
|
+
# Compute messages for all pairs
|
|
106
|
+
# For each edge (i, j), message = f(node_i, edge_ij, node_j)
|
|
107
|
+
# Expand dimensions for broadcasting
|
|
108
|
+
src_nodes = node_hidden[:, None, :] # (N, 1, H)
|
|
109
|
+
dst_nodes = node_hidden[None, :, :] # (1, N, H)
|
|
110
|
+
|
|
111
|
+
# Broadcast to (N, N, H)
|
|
112
|
+
src_expanded = jnp.broadcast_to(src_nodes, (num_nodes, num_nodes, self.hidden_dim))
|
|
113
|
+
dst_expanded = jnp.broadcast_to(dst_nodes, (num_nodes, num_nodes, self.hidden_dim))
|
|
114
|
+
|
|
115
|
+
# Concatenate [src, edge, dst]
|
|
116
|
+
message_input = jnp.concatenate([src_expanded, edge_hidden, dst_expanded], axis=-1)
|
|
117
|
+
|
|
118
|
+
# Compute messages
|
|
119
|
+
messages = nnx.relu(self.message_layer(message_input))
|
|
120
|
+
|
|
121
|
+
# Mask messages by adjacency (only neighbors contribute)
|
|
122
|
+
masked_messages = messages * adjacency[:, :, None]
|
|
123
|
+
|
|
124
|
+
# Aggregate messages (sum over neighbors)
|
|
125
|
+
aggregated = jnp.sum(masked_messages, axis=1) # (N, H)
|
|
126
|
+
|
|
127
|
+
# Update node representations
|
|
128
|
+
update_input = jnp.concatenate([node_hidden, aggregated], axis=-1)
|
|
129
|
+
updated = nnx.relu(self.update_layer(update_input))
|
|
130
|
+
|
|
131
|
+
return updated
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class StackedMessagePassing(nnx.Module):
|
|
135
|
+
"""Stack of message passing layers.
|
|
136
|
+
|
|
137
|
+
Applies multiple rounds of message passing to capture higher-order
|
|
138
|
+
neighborhood information.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
hidden_dim: int,
|
|
144
|
+
num_layers: int,
|
|
145
|
+
in_features: int = 4,
|
|
146
|
+
num_edge_features: int = 4,
|
|
147
|
+
*,
|
|
148
|
+
rngs: nnx.Rngs,
|
|
149
|
+
):
|
|
150
|
+
"""Initialize stacked message passing.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
hidden_dim: Hidden dimension for all layers.
|
|
154
|
+
num_layers: Number of message passing iterations.
|
|
155
|
+
in_features: Number of input node features (default 4 for tests).
|
|
156
|
+
num_edge_features: Number of edge features.
|
|
157
|
+
rngs: Flax NNX random number generators.
|
|
158
|
+
"""
|
|
159
|
+
self.hidden_dim = hidden_dim
|
|
160
|
+
self.num_layers = num_layers
|
|
161
|
+
self.in_features = in_features
|
|
162
|
+
|
|
163
|
+
# Build layers with proper input dimensions:
|
|
164
|
+
# - First layer: in_features -> hidden_dim
|
|
165
|
+
# - Subsequent layers: hidden_dim -> hidden_dim
|
|
166
|
+
layers = []
|
|
167
|
+
for i in range(num_layers):
|
|
168
|
+
layer_in_features = in_features if i == 0 else hidden_dim
|
|
169
|
+
layers.append(
|
|
170
|
+
MessagePassingLayer(
|
|
171
|
+
hidden_dim=hidden_dim,
|
|
172
|
+
in_features=layer_in_features,
|
|
173
|
+
num_edge_features=num_edge_features,
|
|
174
|
+
rngs=rngs,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
self.layers = nnx.List(layers)
|
|
178
|
+
|
|
179
|
+
def __call__(
|
|
180
|
+
self,
|
|
181
|
+
node_features: jnp.ndarray,
|
|
182
|
+
adjacency: jnp.ndarray,
|
|
183
|
+
edge_features: jnp.ndarray | None = None,
|
|
184
|
+
) -> jnp.ndarray:
|
|
185
|
+
"""Apply multiple rounds of message passing.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
node_features: Initial node features.
|
|
189
|
+
adjacency: Adjacency matrix.
|
|
190
|
+
edge_features: Optional edge features.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Final node representations.
|
|
194
|
+
"""
|
|
195
|
+
h = node_features
|
|
196
|
+
|
|
197
|
+
for layer in self.layers:
|
|
198
|
+
h = layer(h, adjacency, edge_features)
|
|
199
|
+
|
|
200
|
+
return h
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Primitive functions for molecular graph processing.
|
|
2
|
+
|
|
3
|
+
This module provides utility functions for converting SMILES strings to
|
|
4
|
+
molecular graphs suitable for differentiable neural network processing.
|
|
5
|
+
RDKit is used for parsing only; all graph operations use JAX arrays.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import jax.numpy as jnp
|
|
13
|
+
from rdkit import Chem
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class AtomFeatureConfig:
|
|
20
|
+
"""Configuration for atom feature extraction.
|
|
21
|
+
|
|
22
|
+
The default configuration produces 34 features:
|
|
23
|
+
|
|
24
|
+
- Atom type: 12 dimensions (C, N, O, S, F, Cl, Br, I, P, Si, B, Other)
|
|
25
|
+
- Degree: 7 dimensions (0-6)
|
|
26
|
+
- Formal charge: 5 dimensions (-2 to +2)
|
|
27
|
+
- Hybridization: 4 dimensions (SP, SP2, SP3, SP3D)
|
|
28
|
+
- Aromaticity: 1 dimension (binary)
|
|
29
|
+
- Num hydrogens: 5 dimensions (0-4)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
num_atom_types: int = 12
|
|
33
|
+
max_degree: int = 6 # Creates max_degree + 1 dimensions
|
|
34
|
+
charge_range: tuple[int, int] = (-2, 2) # Creates 5 dimensions
|
|
35
|
+
num_hybridization_types: int = 4
|
|
36
|
+
max_num_hydrogens: int = 4 # Creates max_num_hydrogens + 1 dimensions
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def total_features(self) -> int:
|
|
40
|
+
"""Calculate total number of atom features."""
|
|
41
|
+
return (
|
|
42
|
+
self.num_atom_types
|
|
43
|
+
+ (self.max_degree + 1)
|
|
44
|
+
+ (self.charge_range[1] - self.charge_range[0] + 1)
|
|
45
|
+
+ self.num_hybridization_types
|
|
46
|
+
+ 1 # aromaticity
|
|
47
|
+
+ (self.max_num_hydrogens + 1)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Default configuration
|
|
52
|
+
DEFAULT_ATOM_CONFIG = AtomFeatureConfig()
|
|
53
|
+
DEFAULT_ATOM_FEATURES = DEFAULT_ATOM_CONFIG.total_features # 34 features
|
|
54
|
+
|
|
55
|
+
# Atom type vocabulary for one-hot encoding
|
|
56
|
+
ATOM_TYPES = ["C", "N", "O", "S", "F", "Cl", "Br", "I", "P", "Si", "B", "Other"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_atom_features(atom: Any, config: AtomFeatureConfig | None = None) -> jnp.ndarray:
|
|
60
|
+
"""Extract features from an RDKit atom object.
|
|
61
|
+
|
|
62
|
+
Features include:
|
|
63
|
+
- Atom type (one-hot, config.num_atom_types)
|
|
64
|
+
- Degree (one-hot, 0 to config.max_degree)
|
|
65
|
+
- Formal charge (one-hot, config.charge_range)
|
|
66
|
+
- Hybridization (one-hot, config.num_hybridization_types)
|
|
67
|
+
- Aromaticity (binary)
|
|
68
|
+
- Number of hydrogens (one-hot, 0 to config.max_num_hydrogens)
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
atom: RDKit atom object.
|
|
72
|
+
config: Feature extraction configuration. Defaults to DEFAULT_ATOM_CONFIG.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Feature vector of shape (config.total_features,).
|
|
76
|
+
"""
|
|
77
|
+
if config is None:
|
|
78
|
+
config = DEFAULT_ATOM_CONFIG
|
|
79
|
+
|
|
80
|
+
features: list[float] = []
|
|
81
|
+
|
|
82
|
+
# Atom type
|
|
83
|
+
symbol = atom.GetSymbol()
|
|
84
|
+
atom_type_idx = ATOM_TYPES.index(symbol) if symbol in ATOM_TYPES else len(ATOM_TYPES) - 1
|
|
85
|
+
atom_type_onehot = [0.0] * config.num_atom_types
|
|
86
|
+
atom_type_onehot[min(atom_type_idx, config.num_atom_types - 1)] = 1.0
|
|
87
|
+
features.extend(atom_type_onehot)
|
|
88
|
+
|
|
89
|
+
# Degree
|
|
90
|
+
degree = min(atom.GetDegree(), config.max_degree)
|
|
91
|
+
degree_onehot = [0.0] * (config.max_degree + 1)
|
|
92
|
+
degree_onehot[degree] = 1.0
|
|
93
|
+
features.extend(degree_onehot)
|
|
94
|
+
|
|
95
|
+
# Formal charge
|
|
96
|
+
charge = atom.GetFormalCharge()
|
|
97
|
+
charge_min, charge_max = config.charge_range
|
|
98
|
+
charge_idx = max(charge_min, min(charge_max, charge)) - charge_min
|
|
99
|
+
charge_dim = charge_max - charge_min + 1
|
|
100
|
+
charge_onehot = [0.0] * charge_dim
|
|
101
|
+
charge_onehot[charge_idx] = 1.0
|
|
102
|
+
features.extend(charge_onehot)
|
|
103
|
+
|
|
104
|
+
# Hybridization
|
|
105
|
+
hybridization = atom.GetHybridization()
|
|
106
|
+
hyb_types = [
|
|
107
|
+
Chem.rdchem.HybridizationType.SP,
|
|
108
|
+
Chem.rdchem.HybridizationType.SP2,
|
|
109
|
+
Chem.rdchem.HybridizationType.SP3,
|
|
110
|
+
Chem.rdchem.HybridizationType.SP3D,
|
|
111
|
+
]
|
|
112
|
+
hyb_onehot = [0.0] * config.num_hybridization_types
|
|
113
|
+
for i, h in enumerate(hyb_types[: config.num_hybridization_types]):
|
|
114
|
+
if hybridization == h:
|
|
115
|
+
hyb_onehot[i] = 1.0
|
|
116
|
+
break
|
|
117
|
+
features.extend(hyb_onehot)
|
|
118
|
+
|
|
119
|
+
# Aromaticity
|
|
120
|
+
features.append(1.0 if atom.GetIsAromatic() else 0.0)
|
|
121
|
+
|
|
122
|
+
# Number of hydrogens
|
|
123
|
+
num_h = min(atom.GetTotalNumHs(), config.max_num_hydrogens)
|
|
124
|
+
h_onehot = [0.0] * (config.max_num_hydrogens + 1)
|
|
125
|
+
h_onehot[num_h] = 1.0
|
|
126
|
+
features.extend(h_onehot)
|
|
127
|
+
|
|
128
|
+
return jnp.array(features, dtype=jnp.float32)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_bond_features(bond: Any) -> jnp.ndarray:
|
|
132
|
+
"""Extract features from an RDKit bond object.
|
|
133
|
+
|
|
134
|
+
Features include:
|
|
135
|
+
- Bond type (one-hot: single, double, triple, aromatic)
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
bond: RDKit bond object.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Feature vector of shape (4,).
|
|
142
|
+
"""
|
|
143
|
+
bond_type = bond.GetBondType()
|
|
144
|
+
features = [
|
|
145
|
+
1 if bond_type == Chem.rdchem.BondType.SINGLE else 0,
|
|
146
|
+
1 if bond_type == Chem.rdchem.BondType.DOUBLE else 0,
|
|
147
|
+
1 if bond_type == Chem.rdchem.BondType.TRIPLE else 0,
|
|
148
|
+
1 if bond_type == Chem.rdchem.BondType.AROMATIC else 0,
|
|
149
|
+
]
|
|
150
|
+
return jnp.array(features, dtype=jnp.float32)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def smiles_to_graph(smiles: str) -> dict[str, Any]:
|
|
154
|
+
"""Convert a SMILES string to a molecular graph.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
smiles: SMILES string representing a molecule.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Dictionary containing:
|
|
161
|
+
- node_features: (num_atoms, num_features) atom feature matrix
|
|
162
|
+
- adjacency: (num_atoms, num_atoms) adjacency matrix
|
|
163
|
+
- edge_features: (num_atoms, num_atoms, num_edge_features) bond features
|
|
164
|
+
- num_nodes: number of atoms
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If SMILES string is invalid.
|
|
168
|
+
"""
|
|
169
|
+
mol = Chem.MolFromSmiles(smiles)
|
|
170
|
+
if mol is None:
|
|
171
|
+
raise ValueError(f"Invalid SMILES string: {smiles}")
|
|
172
|
+
|
|
173
|
+
num_atoms = mol.GetNumAtoms()
|
|
174
|
+
|
|
175
|
+
# Extract node features
|
|
176
|
+
node_features = []
|
|
177
|
+
for atom in mol.GetAtoms():
|
|
178
|
+
node_features.append(get_atom_features(atom))
|
|
179
|
+
node_features = jnp.stack(node_features)
|
|
180
|
+
|
|
181
|
+
# Build adjacency matrix and edge features
|
|
182
|
+
adjacency = jnp.zeros((num_atoms, num_atoms), dtype=jnp.float32)
|
|
183
|
+
edge_features = jnp.zeros((num_atoms, num_atoms, 4), dtype=jnp.float32)
|
|
184
|
+
|
|
185
|
+
for bond in mol.GetBonds():
|
|
186
|
+
i = bond.GetBeginAtomIdx()
|
|
187
|
+
j = bond.GetEndAtomIdx()
|
|
188
|
+
bond_feat = get_bond_features(bond)
|
|
189
|
+
|
|
190
|
+
# Symmetric (undirected graph)
|
|
191
|
+
adjacency = adjacency.at[i, j].set(1.0)
|
|
192
|
+
adjacency = adjacency.at[j, i].set(1.0)
|
|
193
|
+
edge_features = edge_features.at[i, j].set(bond_feat)
|
|
194
|
+
edge_features = edge_features.at[j, i].set(bond_feat)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
"node_features": node_features,
|
|
198
|
+
"adjacency": adjacency,
|
|
199
|
+
"edge_features": edge_features,
|
|
200
|
+
"num_nodes": num_atoms,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def batch_smiles_to_graphs(smiles_list: list[str]) -> dict[str, Any]:
|
|
205
|
+
"""Convert a batch of SMILES strings to padded graph tensors.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
smiles_list: List of SMILES strings.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dictionary containing:
|
|
212
|
+
- node_features: (batch_size, max_nodes, num_features)
|
|
213
|
+
- adjacency: (batch_size, max_nodes, max_nodes)
|
|
214
|
+
- edge_features: (batch_size, max_nodes, max_nodes, num_edge_features)
|
|
215
|
+
- node_mask: (batch_size, max_nodes) mask for valid nodes
|
|
216
|
+
"""
|
|
217
|
+
graphs = [smiles_to_graph(s) for s in smiles_list]
|
|
218
|
+
|
|
219
|
+
max_nodes = max(g["num_nodes"] for g in graphs)
|
|
220
|
+
batch_size = len(graphs)
|
|
221
|
+
num_features = graphs[0]["node_features"].shape[1]
|
|
222
|
+
num_edge_features = graphs[0]["edge_features"].shape[2]
|
|
223
|
+
|
|
224
|
+
# Initialize padded tensors
|
|
225
|
+
node_features = jnp.zeros((batch_size, max_nodes, num_features))
|
|
226
|
+
adjacency = jnp.zeros((batch_size, max_nodes, max_nodes))
|
|
227
|
+
edge_features = jnp.zeros((batch_size, max_nodes, max_nodes, num_edge_features))
|
|
228
|
+
node_mask = jnp.zeros((batch_size, max_nodes))
|
|
229
|
+
|
|
230
|
+
for i, g in enumerate(graphs):
|
|
231
|
+
n = g["num_nodes"]
|
|
232
|
+
node_features = node_features.at[i, :n, :].set(g["node_features"])
|
|
233
|
+
adjacency = adjacency.at[i, :n, :n].set(g["adjacency"])
|
|
234
|
+
edge_features = edge_features.at[i, :n, :n, :].set(g["edge_features"])
|
|
235
|
+
node_mask = node_mask.at[i, :n].set(1.0)
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
"node_features": node_features,
|
|
239
|
+
"adjacency": adjacency,
|
|
240
|
+
"edge_features": edge_features,
|
|
241
|
+
"node_mask": node_mask,
|
|
242
|
+
}
|