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

Commit f8e8cf0

Browse files
authored
Merge pull request #27 from NatLabRockies/TFv220
Tensorflow v2.20
2 parents bdc69cb + 8bc0824 commit f8e8cf0

6 files changed

Lines changed: 121 additions & 15 deletions

File tree

etc/environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dependencies:
88
- rdkit
99
- pytest
1010
- tqdm
11-
- numpy=1.19
11+
- numpy
1212
- networkx
1313
- pymatgen
1414
- pip

nfp/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ class MissingDependencyException(RuntimeError):
88

99

1010
custom_objects = {
11+
"RBFExpansion": RBFExpansion,
1112
"Gather": Gather,
1213
"Reduce": Reduce,
14+
"ConcatDense": ConcatDense,
15+
"Tile": Tile,
1316
"masked_mean_squared_error": masked_mean_squared_error,
1417
"masked_mean_absolute_error": masked_mean_absolute_error,
1518
"masked_log_cosh": masked_log_cosh,

nfp/layers/graph_layers.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
assert tf, "Tensorflow 2.x required for GraphLayers"
55
tf_layers = tf.keras.layers
6+
register_keras_serializable = tf.keras.utils.register_keras_serializable
67

78

9+
@register_keras_serializable(package="nfp")
810
class GraphLayer(tf_layers.Layer):
911
"""Base class for all GNN layers"""
1012

@@ -28,9 +30,16 @@ def build(self, input_shape):
2830
self.dropout_layer = tf_layers.Dropout(self.dropout)
2931

3032
def get_config(self):
31-
return {"dropout": self.dropout}
33+
config = super().get_config()
34+
config.update({"dropout": self.dropout})
35+
return config
36+
37+
@classmethod
38+
def from_config(cls, config):
39+
return cls(**config)
3240

3341

42+
@register_keras_serializable(package="nfp")
3443
class EdgeUpdate(GraphLayer):
3544
def build(self, input_shape):
3645
"""inputs = [atom_state, bond_state, connectivity]
@@ -40,6 +49,10 @@ def build(self, input_shape):
4049

4150
self.gather = nfp.Gather()
4251
self.concat = nfp.ConcatDense()
52+
concat_shapes = [input_shape[1], input_shape[0], input_shape[0]]
53+
if self.use_global:
54+
concat_shapes.append((input_shape[1][0], input_shape[1][1], input_shape[3][-1]))
55+
self.concat.build(concat_shapes)
4356

4457
def call(self, inputs, mask=None, **kwargs):
4558
"""Inputs: [atom_state, bond_state, connectivity]
@@ -77,7 +90,12 @@ def compute_mask(self, inputs, mask=None):
7790
else:
7891
return None
7992

93+
@classmethod
94+
def from_config(cls, config):
95+
return cls(**config)
96+
8097

98+
@register_keras_serializable(package="nfp")
8199
class NodeUpdate(GraphLayer):
82100
def build(self, input_shape):
83101
super().build(input_shape)
@@ -91,6 +109,12 @@ def build(self, input_shape):
91109

92110
self.dense1 = tf_layers.Dense(2 * num_features, activation="relu")
93111
self.dense2 = tf_layers.Dense(num_features)
112+
concat_shapes = [input_shape[0], input_shape[1]]
113+
if self.use_global:
114+
concat_shapes.append((input_shape[1][0], input_shape[1][1], input_shape[3][-1]))
115+
self.concat.build(concat_shapes)
116+
self.dense1.build((None, num_features))
117+
self.dense2.build((None, 2 * num_features))
94118

95119
def call(self, inputs, mask=None, **kwargs):
96120
"""Inputs: [atom_state, bond_state, connectivity]
@@ -110,7 +134,7 @@ def call(self, inputs, mask=None, **kwargs):
110134
else:
111135
messages = self.concat([source_atom, bond_state, global_state])
112136

113-
if mask is not None:
137+
if mask is not None and mask[1] is not None:
114138
# Only works for sum, max
115139
messages = tf.where(
116140
tf.expand_dims(mask[1], axis=-1), messages, tf.zeros_like(messages)
@@ -136,19 +160,27 @@ def compute_mask(self, inputs, mask=None):
136160
else:
137161
return None
138162

163+
@classmethod
164+
def from_config(cls, config):
165+
return cls(**config)
139166

167+
168+
@register_keras_serializable(package="nfp")
140169
class GlobalUpdate(GraphLayer):
141170
def __init__(self, units, num_heads, **kwargs):
142171
super().__init__(**kwargs)
143172
self.units = units # H
144173
self.num_heads = num_heads # N
145-
self.supports_masking = False
174+
self.supports_masking = True
146175

147176
def build(self, input_shape):
148177
super().build(input_shape)
149178
dense_units = self.units * self.num_heads # N*H
179+
num_features = input_shape[0][-1]
150180
self.query_layer = tf_layers.Dense(self.num_heads, name="query")
151181
self.value_layer = tf_layers.Dense(dense_units, name="value")
182+
self.query_layer.build((None, num_features))
183+
self.value_layer.build((None, num_features))
152184

153185
def transpose_scores(self, input_tensor):
154186
input_shape = tf.shape(input_tensor)
@@ -167,7 +199,7 @@ def call(self, inputs, mask=None, **kwargs):
167199
graph_elements = tf.concat([atom_state, bond_state], axis=1)
168200
query = self.query_layer(graph_elements) # [B,N,S,H]
169201

170-
if mask is not None:
202+
if mask is not None and mask[0] is not None and mask[1] is not None:
171203
graph_element_mask = tf.concat([mask[0], mask[1]], axis=1)
172204
query = tf.where(
173205
tf.expand_dims(graph_element_mask, axis=-1),
@@ -187,7 +219,18 @@ def call(self, inputs, mask=None, **kwargs):
187219

188220
return context
189221

222+
def compute_output_shape(self, input_shape):
223+
# input_shape[0] is atom_state: (batch, N_atoms, features)
224+
return (input_shape[0][0], self.num_heads * self.units)
225+
226+
def compute_mask(self, inputs, mask=None):
227+
return None # 2D output has no sequence mask
228+
190229
def get_config(self):
191230
config = super(GlobalUpdate, self).get_config()
192231
config.update({"units": self.units, "num_heads": self.num_heads})
193232
return config
233+
234+
@classmethod
235+
def from_config(cls, config):
236+
return cls(**config)

nfp/layers/layers.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
assert tf, "Tensorflow 2.x required for GraphLayers"
44
tf_layers = tf.keras.layers
5+
register_keras_serializable = tf.keras.utils.register_keras_serializable
56

67

8+
@register_keras_serializable(package="nfp")
79
class RBFExpansion(tf_layers.Layer):
810
def __init__(
911
self, dimension=128, init_gap=10, init_max_distance=7, trainable=False
@@ -55,12 +57,18 @@ def compute_mask(self, inputs, mask=None):
5557
return tf.logical_not(tf.math.is_nan(inputs))
5658

5759
def get_config(self):
58-
return {
60+
config = super().get_config()
61+
config.update({
5962
"init_gap": self.init_gap,
6063
"init_max_distance": self.init_max_distance,
6164
"dimension": self.dimension,
6265
"trainable": self.trainable,
63-
}
66+
})
67+
return config
68+
69+
@classmethod
70+
def from_config(cls, config):
71+
return cls(**config)
6472

6573

6674
def batched_segment_op(
@@ -117,16 +125,33 @@ def batched_segment_op(
117125
# return cls(**config)
118126

119127

128+
@register_keras_serializable(package="nfp")
120129
class Gather(tf_layers.Layer):
130+
def __init__(self, **kwargs):
131+
super().__init__(**kwargs)
132+
self.supports_masking = True
133+
121134
def call(self, inputs, mask=None, **kwargs):
122135
reference, indices = inputs
123136
return tf.gather(reference, indices, batch_dims=1)
124137

138+
def compute_mask(self, inputs, mask=None):
139+
return None
125140

141+
def get_config(self):
142+
return super().get_config()
143+
144+
@classmethod
145+
def from_config(cls, config):
146+
return cls(**config)
147+
148+
149+
@register_keras_serializable(package="nfp")
126150
class Reduce(tf_layers.Layer):
127151
def __init__(self, reduction="sum", *args, **kwargs):
128152
super(Reduce, self).__init__(*args, **kwargs)
129153
self.reduction = reduction
154+
self.supports_masking = True
130155

131156
def compute_output_shape(self, input_shape):
132157
data_shape, _, target_shape = input_shape
@@ -143,8 +168,17 @@ def call(self, inputs, mask=None, **kwargs):
143168
reduction=self.reduction,
144169
)
145170

171+
def compute_mask(self, inputs, mask=None):
172+
return None
173+
146174
def get_config(self):
147-
return {"reduction": self.reduction}
175+
config = super().get_config()
176+
config.update({"reduction": self.reduction})
177+
return config
178+
179+
@classmethod
180+
def from_config(cls, config):
181+
return cls(**config)
148182

149183
@staticmethod
150184
def _parse_inputs_and_mask(inputs, mask=None):
@@ -159,6 +193,7 @@ def _parse_inputs_and_mask(inputs, mask=None):
159193
return data, segment_ids, target, data_mask
160194

161195

196+
@register_keras_serializable(package="nfp")
162197
class ConcatDense(tf_layers.Layer):
163198
"""Layer to combine the concatenation and two dense layers. Just useful as a common
164199
operation in the graph layers"""
@@ -169,23 +204,41 @@ def __init__(self, **kwargs):
169204

170205
def build(self, input_shape):
171206
num_features = input_shape[0][-1]
207+
concat_features = sum(shape[-1] for shape in input_shape)
172208
self.concat = tf_layers.Concatenate()
173209
self.dense1 = tf_layers.Dense(2 * num_features, activation="relu")
174210
self.dense2 = tf_layers.Dense(num_features)
211+
self.dense1.build((None, concat_features))
212+
self.dense2.build((None, 2 * num_features))
213+
super().build(input_shape)
175214

176215
def call(self, inputs, mask=None, **kwargs):
177-
output = self.concat(inputs)
216+
# Use tf.concat instead of Keras Concatenate to avoid Keras 3's
217+
# compute_mask creating an all-True mask when some inputs lack masks
218+
output = tf.concat(inputs, axis=-1)
178219
output = self.dense1(output)
179220
output = self.dense2(output)
180221
return output
181222

182223
def compute_mask(self, inputs, mask=None):
183224
if mask is None:
184225
return None
185-
else:
186-
return tf.math.reduce_all(tf.stack(mask), axis=0)
226+
valid_masks = [m for m in mask if m is not None]
227+
if not valid_masks:
228+
return None
229+
if len(valid_masks) == 1:
230+
return valid_masks[0]
231+
return tf.math.reduce_all(tf.stack(valid_masks), axis=0)
232+
233+
def get_config(self):
234+
return super().get_config()
235+
236+
@classmethod
237+
def from_config(cls, config):
238+
return cls(**config)
187239

188240

241+
@register_keras_serializable(package="nfp")
189242
class Tile(tf_layers.Layer):
190243
def __init__(self, **kwargs):
191244
super(Tile, self).__init__(**kwargs)
@@ -202,3 +255,10 @@ def compute_mask(self, inputs, mask=None):
202255
return None
203256
else:
204257
return mask[1]
258+
259+
def get_config(self):
260+
return super().get_config()
261+
262+
@classmethod
263+
def from_config(cls, config):
264+
return cls(**config)

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ install_requires =
1313

1414
[options.extras_require]
1515
pymatgen = pymatgen
16-
tf = tensorflow>=2.4
16+
tf = tensorflow>=2.20
1717
rdkit = rdkit-pypi
1818
test = pytest
1919

2020
[flake8]
2121
max-line-length = 88
22-
extend-ignore = E203
22+
extend-ignore = E203

tests/layers/test_layers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424

2525
def test_gather():
26-
in1 = layers.Input(shape=[None], dtype="float", name="data")
26+
in1 = layers.Input(shape=[None], dtype="float32", name="data")
2727
in2 = layers.Input(shape=[None], dtype=tf.int64, name="indices")
2828

2929
gather = nfp.Gather()([in1, in2])
@@ -73,7 +73,7 @@ def test_reduce(smiles_inputs, method):
7373

7474

7575
def test_tile():
76-
state = layers.Input(shape=[None], dtype="float", name="data")
76+
state = layers.Input(shape=[None], dtype="float32", name="data")
7777
target = layers.Input(shape=[None, 3], dtype=tf.int64, name="indices")
7878

7979
tile = nfp.Tile()([state, target])

0 commit comments

Comments
 (0)