Skip to content
This repository was archived by the owner on Jun 30, 2026. It is now read-only.

Commit 32cbb54

Browse files
authored
Merge pull request #13 from NREL/tf-2.6
new preprocessor inheritance model
2 parents e4ed176 + 1ea10ef commit 32cbb54

22 files changed

Lines changed: 762 additions & 412 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,4 @@ examples/*.tfrecord
111111
examples/*.p
112112
examples/*.h5
113113
.idea/
114+
.vscode/settings.json

.vscode/extensions.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"recommendations": [
3+
"ms-python.python"
4+
]
5+
}

etc/environment.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ dependencies:
88
- rdkit
99
- pytest
1010
- tqdm
11-
- numpy
11+
- numpy=1.19
12+
- networkx
13+
- pymatgen
1214
- pip
1315
- pip:
14-
- tensorflow
16+
- tensorflow

examples/creating_and_training_a_model.ipynb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@
232232
"print(preprocessor.atom_tokenizer._data)\n",
233233
"\n",
234234
"for smiles in train:\n",
235-
" preprocessor.construct_feature_matrices(smiles, train=True)\n",
235+
" preprocessor(smiles, train=True)\n",
236236
" \n",
237237
"print()\n",
238238
"print(\"after pre-allocating\")\n",
@@ -260,7 +260,7 @@
260260
"smiles = 'CCO'\n",
261261
"\n",
262262
"# Atom types, as integer classes\n",
263-
"preprocessor.construct_feature_matrices(smiles, train=True)['atom']"
263+
"preprocessor(smiles, train=True)['atom']"
264264
]
265265
},
266266
{
@@ -281,7 +281,7 @@
281281
],
282282
"source": [
283283
"# Bond types, as integer classes\n",
284-
"preprocessor.construct_feature_matrices(smiles, train=True)['bond']"
284+
"preprocessor(smiles, train=True)['bond']"
285285
]
286286
},
287287
{
@@ -305,7 +305,7 @@
305305
],
306306
"source": [
307307
"# A connectivity array, where row i indicates bond i connects atom j to atom k\n",
308-
"preprocessor.construct_feature_matrices(smiles, train=True)['connectivity']"
308+
"preprocessor(smiles, train=True)['connectivity']"
309309
]
310310
},
311311
{
@@ -322,7 +322,7 @@
322322
"# hence why the atom and bond types above start with 1 as the unknown class)\n",
323323
"\n",
324324
"train_dataset = tf.data.Dataset.from_generator(\n",
325-
" lambda: ((preprocessor.construct_feature_matrices(row.SMILES, train=False), row.YSI)\n",
325+
" lambda: ((preprocessor(row.SMILES, train=False), row.YSI)\n",
326326
" for i, row in ysi[ysi.SMILES.isin(train)].iterrows()),\n",
327327
" output_types=(preprocessor.output_types, tf.float32),\n",
328328
" output_shapes=(preprocessor.output_shapes, []))\\\n",
@@ -334,7 +334,7 @@
334334
"\n",
335335
"\n",
336336
"valid_dataset = tf.data.Dataset.from_generator(\n",
337-
" lambda: ((preprocessor.construct_feature_matrices(row.SMILES, train=False), row.YSI)\n",
337+
" lambda: ((preprocessor(row.SMILES, train=False), row.YSI)\n",
338338
" for i, row in ysi[ysi.SMILES.isin(valid)].iterrows()),\n",
339339
" output_types=(preprocessor.output_types, tf.float32),\n",
340340
" output_shapes=(preprocessor.output_shapes, []))\\\n",
@@ -498,7 +498,7 @@
498498
"# Here, we create a test dataset that doesn't assume we know the values for the YSI\n",
499499
"\n",
500500
"test_dataset = tf.data.Dataset.from_generator(\n",
501-
" lambda: (preprocessor.construct_feature_matrices(smiles, train=False)\n",
501+
" lambda: (preprocessor(smiles, train=False)\n",
502502
" for smiles in test),\n",
503503
" output_types=preprocessor.output_types,\n",
504504
" output_shapes=preprocessor.output_shapes)\\\n",

nfp/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from .preprocessing import *
66

77
custom_objects = {
8-
'Slice': Slice,
98
'Gather': Gather,
109
'Reduce': Reduce,
1110
'masked_mean_squared_error': masked_mean_squared_error,

nfp/layers/graph_layers.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import numpy as np
21
import tensorflow as tf
32
from tensorflow.keras import layers
43

@@ -39,13 +38,12 @@ def build(self, input_shape):
3938
super().build(input_shape)
4039

4140
self.gather = nfp.Gather()
42-
self.slice1 = nfp.Slice(np.s_[:, :, 1])
43-
self.slice0 = nfp.Slice(np.s_[:, :, 0])
4441
self.concat = nfp.ConcatDense()
4542

46-
def call(self, inputs, mask=None):
43+
def call(self, inputs, mask=None, **kwargs):
4744
""" Inputs: [atom_state, bond_state, connectivity]
4845
Outputs: bond_state
46+
4947
"""
5048
if not self.use_global:
5149
atom_state, bond_state, connectivity = inputs
@@ -54,13 +52,15 @@ def call(self, inputs, mask=None):
5452
global_state = self.tile([global_state, bond_state])
5553

5654
# Get nodes at start and end of edge
57-
source_atom = self.gather([atom_state, self.slice1(connectivity)])
58-
target_atom = self.gather([atom_state, self.slice0(connectivity)])
55+
source_atom = self.gather([atom_state, connectivity[:, :, 0]])
56+
target_atom = self.gather([atom_state, connectivity[:, :, 1]])
5957

6058
if not self.use_global:
61-
new_bond_state = self.concat([bond_state, source_atom, target_atom])
59+
new_bond_state = self.concat(
60+
[bond_state, source_atom, target_atom])
6261
else:
63-
new_bond_state = self.concat([bond_state, source_atom, target_atom, global_state])
62+
new_bond_state = self.concat(
63+
[bond_state, source_atom, target_atom, global_state])
6464

6565
if self.dropout > 0.:
6666
new_bond_state = self.dropout_layer(new_bond_state)
@@ -84,26 +84,25 @@ def build(self, input_shape):
8484
num_features = input_shape[1][-1]
8585

8686
self.gather = nfp.Gather()
87-
self.slice0 = nfp.Slice(np.s_[:, :, 0])
88-
self.slice1 = nfp.Slice(np.s_[:, :, 1])
8987

9088
self.concat = nfp.ConcatDense()
9189
self.reduce = nfp.Reduce(reduction='sum')
9290

9391
self.dense1 = layers.Dense(2 * num_features, activation='relu')
9492
self.dense2 = layers.Dense(num_features)
9593

96-
def call(self, inputs, mask=None):
94+
def call(self, inputs, mask=None, **kwargs):
9795
""" Inputs: [atom_state, bond_state, connectivity]
9896
Outputs: atom_state
97+
9998
"""
10099
if not self.use_global:
101100
atom_state, bond_state, connectivity = inputs
102101
else:
103102
atom_state, bond_state, connectivity, global_state = inputs
104103
global_state = self.tile([global_state, bond_state])
105104

106-
source_atom = self.gather([atom_state, self.slice1(connectivity)])
105+
source_atom = self.gather([atom_state, connectivity[:, :, 1]])
107106

108107
if not self.use_global:
109108
messages = self.concat([source_atom, bond_state])
@@ -112,10 +111,11 @@ def call(self, inputs, mask=None):
112111

113112
if mask is not None:
114113
# Only works for sum, max
115-
messages = tf.where(tf.expand_dims(mask[1], axis=-1),
116-
messages, tf.zeros_like(messages))
114+
messages = tf.where(tf.expand_dims(mask[1], axis=-1), messages,
115+
tf.zeros_like(messages))
117116

118-
new_atom_state = self.reduce([messages, self.slice0(connectivity), atom_state])
117+
new_atom_state = self.reduce(
118+
[messages, connectivity[:, :, 0], atom_state])
119119

120120
# Dense net after message reduction
121121
new_atom_state = self.dense1(new_atom_state)
@@ -151,11 +151,13 @@ def build(self, input_shape):
151151

152152
def transpose_scores(self, input_tensor):
153153
input_shape = tf.shape(input_tensor)
154-
output_shape = [input_shape[0], input_shape[1], self.num_heads, self.units]
154+
output_shape = [
155+
input_shape[0], input_shape[1], self.num_heads, self.units
156+
]
155157
output_tensor = tf.reshape(input_tensor, output_shape)
156158
return tf.transpose(a=output_tensor, perm=[0, 2, 1, 3]) # [B,N,S,H]
157159

158-
def call(self, inputs, mask=None):
160+
def call(self, inputs, mask=None, **kwargs):
159161
if not self.use_global:
160162
atom_state, bond_state, connectivity = inputs
161163
else:
@@ -168,17 +170,18 @@ def call(self, inputs, mask=None):
168170

169171
if mask is not None:
170172
graph_element_mask = tf.concat([mask[0], mask[1]], axis=1)
171-
query = tf.where(
172-
tf.expand_dims(graph_element_mask, axis=-1),
173-
query,
174-
tf.ones_like(query) * query.dtype.min)
173+
query = tf.where(tf.expand_dims(graph_element_mask, axis=-1),
174+
query,
175+
tf.ones_like(query) * query.dtype.min)
175176

176177
query = tf.transpose(query, perm=[0, 2, 1])
177-
value = self.transpose_scores(self.value_layer(graph_elements)) # [B,N,S,H]
178+
value = self.transpose_scores(
179+
self.value_layer(graph_elements)) # [B,N,S,H]
178180

179181
attention_probs = tf.nn.softmax(query)
180182
context = tf.matmul(tf.expand_dims(attention_probs, 2), value)
181-
context = tf.reshape(context, [batch_size, self.num_heads * self.units])
183+
context = tf.reshape(context,
184+
[batch_size, self.num_heads * self.units])
182185

183186
if self.dropout > 0.:
184187
context = self.dropout_layer(context)
@@ -187,7 +190,5 @@ def call(self, inputs, mask=None):
187190

188191
def get_config(self):
189192
config = super(GlobalUpdate, self).get_config()
190-
config.update(
191-
{"units": self.units,
192-
"num_heads": self.num_heads})
193+
config.update({"units": self.units, "num_heads": self.num_heads})
193194
return config

nfp/layers/layers.py

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,66 @@
1-
import logging
2-
31
import tensorflow as tf
42
from tensorflow.keras import layers
53

64

5+
class RBFExpansion(layers.Layer):
6+
def __init__(self,
7+
dimension=128,
8+
init_gap=10,
9+
init_max_distance=7,
10+
trainable=False):
11+
""" Layer to calculate radial basis function 'embeddings' for a continuous input variable. The width and
12+
location of each bin can be optionally trained. Essentially equivalent to a 1-hot embedding for a continous
13+
variable.
14+
15+
Parameters
16+
----------
17+
dimension: The total number of distance bins
18+
init_gap: The initial width of each gaussian distribution
19+
init_max_distance: the initial maximum value of the continuous variable
20+
trainable: Whether the centers and gap parameters should be added as trainable NN parameters.
21+
"""
22+
super(RBFExpansion, self).__init__()
23+
self.init_gap = init_gap
24+
self.init_max_distance = init_max_distance
25+
self.dimension = dimension
26+
self.trainable = trainable
27+
28+
def build(self, input_shape):
29+
self.centers = tf.Variable(
30+
name='centers',
31+
initial_value=tf.range(0,
32+
self.init_max_distance,
33+
delta=self.init_max_distance /
34+
self.dimension),
35+
trainable=self.trainable,
36+
dtype=tf.float32)
37+
38+
self.gap = tf.Variable(name='gap',
39+
initial_value=tf.constant(self.init_gap,
40+
dtype=tf.float32),
41+
trainable=self.trainable,
42+
dtype=tf.float32)
43+
44+
def call(self, inputs, **kwargs):
45+
distances = tf.where(tf.math.is_nan(inputs),
46+
tf.zeros_like(inputs, dtype=inputs.dtype), inputs)
47+
offset = tf.expand_dims(distances, -1) - tf.cast(
48+
self.centers, inputs.dtype)
49+
logits = -self.gap * offset**2
50+
return tf.exp(logits)
51+
52+
def compute_mask(self, inputs, mask=None):
53+
return tf.logical_not(tf.math.is_nan(inputs))
54+
55+
def get_config(self):
56+
return {
57+
'init_gap': self.init_gap,
58+
'init_max_distance': self.init_max_distance,
59+
'dimension': self.dimension,
60+
'trainable': self.trainable
61+
}
62+
63+
764
def batched_segment_op(data,
865
segment_ids,
966
num_segments,
@@ -42,22 +99,22 @@ def batched_segment_op(data,
4299
return tf.reshape(reduced_data, [batch_size, num_segments, data.shape[-1]])
43100

44101

45-
class Slice(layers.Layer):
46-
def __init__(self, slice_obj, *args, **kwargs):
47-
super(Slice, self).__init__(*args, **kwargs)
48-
self.slice_obj = slice_obj
49-
self.supports_masking = True
50-
51-
def call(self, inputs, mask=None):
52-
return inputs[self.slice_obj]
53-
54-
def get_config(self):
55-
return {'slice_obj': str(self.slice_obj)}
56-
57-
@classmethod
58-
def from_config(cls, config):
59-
config['slice_obj'] = eval(config['slice_obj'])
60-
return cls(**config)
102+
# class Slice(layers.Layer):
103+
# def __init__(self, slice_obj, *args, **kwargs):
104+
# super(Slice, self).__init__(*args, **kwargs)
105+
# self.slice_obj = slice_obj
106+
# self.supports_masking = True
107+
#
108+
# def call(self, inputs, mask=None):
109+
# return inputs[self.slice_obj]
110+
#
111+
# def get_config(self):
112+
# return {'slice_obj': str(self.slice_obj)}
113+
#
114+
# @classmethod
115+
# def from_config(cls, config):
116+
# config['slice_obj'] = eval(config['slice_obj'])
117+
# return cls(**config)
61118

62119

63120
class Gather(layers.Layer):
@@ -82,16 +139,19 @@ def _parse_inputs_and_mask(self, inputs, mask=None):
82139

83140
return data, segment_ids, target, data_mask
84141

142+
def compute_output_shape(self, input_shape):
143+
data_shape, _, target_shape = input_shape
144+
return [data_shape[0], target_shape[1], data_shape[-1]]
145+
85146
def call(self, inputs, mask=None):
86147
data, segment_ids, target, data_mask = self._parse_inputs_and_mask(
87148
inputs, mask)
88149
num_segments = tf.shape(target, out_type=segment_ids.dtype)[1]
89-
return batched_segment_op(
90-
data,
91-
segment_ids,
92-
num_segments,
93-
data_mask=data_mask,
94-
reduction=self.reduction)
150+
return batched_segment_op(data,
151+
segment_ids,
152+
num_segments,
153+
data_mask=data_mask,
154+
reduction=self.reduction)
95155

96156
def get_config(self):
97157
return {'reduction': self.reduction}
@@ -100,7 +160,6 @@ def get_config(self):
100160
class ConcatDense(layers.Layer):
101161
""" Layer to combine the concatenation and two dense layers. Just useful as a common operation in the graph
102162
layers """
103-
104163
def __init__(self, **kwargs):
105164
super(ConcatDense, self).__init__(**kwargs)
106165
self.supports_masking = True

nfp/preprocessing/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +0,0 @@
1-
from .features import *
2-
from .preprocessor import *
3-
from .tfrecord import *

0 commit comments

Comments
 (0)