The PiNet network

The PiNet network implements the network architecture described in our paper.1 The network architecture features the graph-convolution which recursively generates atomic properties from local environment. One distinctive feature of PiNet is that the convolution operation is realized with pairwise functions whose form are determined by the pair, called pairwise interactions.

Network architecture

The overall architecture of PiNet is illustrated with the illustration below:

PiNet architecture

The preprocess part of the network are implemented with shared layers (see Layers). The graph-convolution (GC) block are further divided into PI and IP operations, each consists several layers. Those operations are recursively applied to update the latent variables, and the output is updated after each iteration (OutLayer).

We classify the latent variables into the atom-centered "properties" (\(\mathbb{P}\)) and the pair-wise "interactions" (\(\mathbb{I}\)) in our notation. Since the layers that transform \(\mathbb{P}\) to \(\mathbb{P}\) or \(\mathbb{I}\) to \(\mathbb{I}\) are usually standard feed-forward neural networks (FFLayer), the special part of PiNet are PILayer and IPLayers, which transform between those two types of variables.

We use the superscript to identify each tensor, and the subscripts to differentiate the indices of different types for each variable, following the convention:

\(\mathbb{P}^{t}_{i\alpha}\) thus denote value of the \(\alpha\)-th channel of the \(i\)-th atom in the tensor \(\mathbb{P}^{t}\). We always provide all the subscripts of a given tensor in the equations below, so that the dimensionality of each tensor is unambiguously implied.

For instance, \(r_{ij}\) entails a scalar distance defined between each pair of atoms, indexed by \(i,j\); \(\mathbb{P}_{i\alpha}\) entails the atomic feature vectors indexed by \(i\) for the atom, and \(\alpha\) for the channel. The equations that explain each of the above layers and the hyperparameters available for the PiNet network are detailed below.

The parameters for PiNet are outlined in the network specification and can be applied in the configuration file as shown in the following snippet:

"network": {
    "name": "PiNet",
    "params": {
        "atom_types": [1, 8],
        "basis_type": "gaussian",
        "depth": 5,
        "ii_nodes": [16, 16, 16, 16],
        "n_basis": 10,
        "out_nodes": [16],
        "pi_nodes": [16],
        "pp_nodes": [16, 16, 16, 16],
        "rc": 6.0,
    }
},

Network specification

pinet.PiNet

Bases: Model

This class implements the Keras Model for the PiNet network.

Source code in pinn/networks/pinet.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
class PiNet(tf.keras.Model):
    """This class implements the Keras Model for the PiNet network."""

    def __init__(
        self,
        atom_types=[1, 6, 7, 8],
        rc=4.0,
        cutoff_type="f1",
        basis_type="polynomial",
        n_basis=4,
        gamma=3.0,
        center=None,
        pp_nodes=[16, 16],
        pi_nodes=[16, 16],
        ii_nodes=[16, 16],
        out_nodes=[16, 16],
        out_units=1,
        out_pool=False,
        act="tanh",
        depth=4,
    ):
        """
        Args:
            atom_types (list): elements for the one-hot embedding
            pp_nodes (list): number of nodes for PPLayer
            pi_nodes (list): number of nodes for PILayer
            ii_nodes (list): number of nodes for IILayer
            out_nodes (list): number of nodes for OutLayer
            out_pool (str): pool atomic outputs, see ANNOutput
            depth (int): number of interaction blocks
            rc (float): cutoff radius
            basis_type (string): basis function, can be "polynomial" or "gaussian"
            n_basis (int): number of basis functions to use
            gamma (float|array): width of gaussian function for gaussian basis
            center (float|array): center of gaussian function for gaussian basis
            cutoff_type (string): cutoff function to use with the basis.
            act (string): activation function to use
        """
        super(PiNet, self).__init__()

        self.depth = depth
        self.preprocess = PreprocessLayer(atom_types, rc)
        self.cutoff = CutoffFunc(rc, cutoff_type)

        if basis_type == "polynomial":
            self.basis_fn = PolynomialBasis(n_basis)
        elif basis_type == "gaussian":
            self.basis_fn = GaussianBasis(center, gamma, rc, n_basis)

        self.res_update = [ResUpdate() for i in range(depth)]
        self.gc_blocks = [GCBlock([], pi_nodes, ii_nodes, activation=act)]
        self.gc_blocks += [
            GCBlock(pp_nodes, pi_nodes, ii_nodes, activation=act)
            for i in range(depth - 1)
        ]
        self.out_layers = [OutLayer(out_nodes, out_units) for i in range(depth)]
        self.ann_output = ANNOutput(out_pool)

    def call(self, tensors):
        """PiNet takes batches atomic data as input, the following keys are
        required in the input dictionary of tensors:

        - `ind_1`: [sparse indices](layers.md#sparse-indices) for the batched data, with shape `(n_atoms, 1)`;
        - `elems`: element (atomic numbers) for each atom, with shape `(n_atoms)`;
        - `coord`: coordintaes for each atom, with shape `(n_atoms, 3)`.

        Optionally, the input dataset can be processed with
        `PiNet.preprocess(tensors)`, which adds the following tensors to the
        dictionary:

        - `ind_2`: [sparse indices](layers.md#sparse-indices) for neighbour list, with shape `(n_pairs, 2)`;
        - `dist`: distances from the neighbour list, with shape `(n_pairs)`;
        - `diff`: distance vectors from the neighbour list, with shape `(n_pairs, 3)`;
        - `prop`: initial properties `(n_pairs, n_elems)`;

        Args:
            tensors (dict of tensors): input tensors

        Returns:
            output (tensor): output tensor with shape `[n_atoms, out_nodes]`
        """
        tensors = self.preprocess(tensors)
        fc = self.cutoff(tensors["dist"])
        basis = self.basis_fn(tensors["dist"], fc=fc)
        output = 0.0
        for i in range(self.depth):
            prop = self.gc_blocks[i]([tensors["ind_2"], tensors["prop"], basis])
            output = self.out_layers[i]([tensors["ind_1"], prop, output])
            tensors["prop"] = self.res_update[i]([tensors["prop"], prop])

        output = self.ann_output([tensors["ind_1"], output])
        return output

__init__(atom_types=[1, 6, 7, 8], rc=4.0, cutoff_type='f1', basis_type='polynomial', n_basis=4, gamma=3.0, center=None, pp_nodes=[16, 16], pi_nodes=[16, 16], ii_nodes=[16, 16], out_nodes=[16, 16], out_units=1, out_pool=False, act='tanh', depth=4)

Parameters:

Name Type Description Default
atom_types list

elements for the one-hot embedding

[1, 6, 7, 8]
pp_nodes list

number of nodes for PPLayer

[16, 16]
pi_nodes list

number of nodes for PILayer

[16, 16]
ii_nodes list

number of nodes for IILayer

[16, 16]
out_nodes list

number of nodes for OutLayer

[16, 16]
out_pool str

pool atomic outputs, see ANNOutput

False
depth int

number of interaction blocks

4
rc float

cutoff radius

4.0
basis_type string

basis function, can be "polynomial" or "gaussian"

'polynomial'
n_basis int

number of basis functions to use

4
gamma float | array

width of gaussian function for gaussian basis

3.0
center float | array

center of gaussian function for gaussian basis

None
cutoff_type string

cutoff function to use with the basis.

'f1'
act string

activation function to use

'tanh'
Source code in pinn/networks/pinet.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
def __init__(
    self,
    atom_types=[1, 6, 7, 8],
    rc=4.0,
    cutoff_type="f1",
    basis_type="polynomial",
    n_basis=4,
    gamma=3.0,
    center=None,
    pp_nodes=[16, 16],
    pi_nodes=[16, 16],
    ii_nodes=[16, 16],
    out_nodes=[16, 16],
    out_units=1,
    out_pool=False,
    act="tanh",
    depth=4,
):
    """
    Args:
        atom_types (list): elements for the one-hot embedding
        pp_nodes (list): number of nodes for PPLayer
        pi_nodes (list): number of nodes for PILayer
        ii_nodes (list): number of nodes for IILayer
        out_nodes (list): number of nodes for OutLayer
        out_pool (str): pool atomic outputs, see ANNOutput
        depth (int): number of interaction blocks
        rc (float): cutoff radius
        basis_type (string): basis function, can be "polynomial" or "gaussian"
        n_basis (int): number of basis functions to use
        gamma (float|array): width of gaussian function for gaussian basis
        center (float|array): center of gaussian function for gaussian basis
        cutoff_type (string): cutoff function to use with the basis.
        act (string): activation function to use
    """
    super(PiNet, self).__init__()

    self.depth = depth
    self.preprocess = PreprocessLayer(atom_types, rc)
    self.cutoff = CutoffFunc(rc, cutoff_type)

    if basis_type == "polynomial":
        self.basis_fn = PolynomialBasis(n_basis)
    elif basis_type == "gaussian":
        self.basis_fn = GaussianBasis(center, gamma, rc, n_basis)

    self.res_update = [ResUpdate() for i in range(depth)]
    self.gc_blocks = [GCBlock([], pi_nodes, ii_nodes, activation=act)]
    self.gc_blocks += [
        GCBlock(pp_nodes, pi_nodes, ii_nodes, activation=act)
        for i in range(depth - 1)
    ]
    self.out_layers = [OutLayer(out_nodes, out_units) for i in range(depth)]
    self.ann_output = ANNOutput(out_pool)

call(tensors)

PiNet takes batches atomic data as input, the following keys are required in the input dictionary of tensors:

  • ind_1: sparse indices for the batched data, with shape (n_atoms, 1);
  • elems: element (atomic numbers) for each atom, with shape (n_atoms);
  • coord: coordintaes for each atom, with shape (n_atoms, 3).

Optionally, the input dataset can be processed with PiNet.preprocess(tensors), which adds the following tensors to the dictionary:

  • ind_2: sparse indices for neighbour list, with shape (n_pairs, 2);
  • dist: distances from the neighbour list, with shape (n_pairs);
  • diff: distance vectors from the neighbour list, with shape (n_pairs, 3);
  • prop: initial properties (n_pairs, n_elems);

Parameters:

Name Type Description Default
tensors dict of tensors

input tensors

required

Returns:

Name Type Description
output tensor

output tensor with shape [n_atoms, out_nodes]

Source code in pinn/networks/pinet.py
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
def call(self, tensors):
    """PiNet takes batches atomic data as input, the following keys are
    required in the input dictionary of tensors:

    - `ind_1`: [sparse indices](layers.md#sparse-indices) for the batched data, with shape `(n_atoms, 1)`;
    - `elems`: element (atomic numbers) for each atom, with shape `(n_atoms)`;
    - `coord`: coordintaes for each atom, with shape `(n_atoms, 3)`.

    Optionally, the input dataset can be processed with
    `PiNet.preprocess(tensors)`, which adds the following tensors to the
    dictionary:

    - `ind_2`: [sparse indices](layers.md#sparse-indices) for neighbour list, with shape `(n_pairs, 2)`;
    - `dist`: distances from the neighbour list, with shape `(n_pairs)`;
    - `diff`: distance vectors from the neighbour list, with shape `(n_pairs, 3)`;
    - `prop`: initial properties `(n_pairs, n_elems)`;

    Args:
        tensors (dict of tensors): input tensors

    Returns:
        output (tensor): output tensor with shape `[n_atoms, out_nodes]`
    """
    tensors = self.preprocess(tensors)
    fc = self.cutoff(tensors["dist"])
    basis = self.basis_fn(tensors["dist"], fc=fc)
    output = 0.0
    for i in range(self.depth):
        prop = self.gc_blocks[i]([tensors["ind_2"], tensors["prop"], basis])
        output = self.out_layers[i]([tensors["ind_1"], prop, output])
        tensors["prop"] = self.res_update[i]([tensors["prop"], prop])

    output = self.ann_output([tensors["ind_1"], output])
    return output

Layer specifications

pinet.FFLayer

Bases: Layer

FFLayer is a shortcut to create a multi-layer perceptron (MLP) or a feed-forward network. A FFLayer takes one tensor as input of arbitratry shape, and parse it to a list of tf.keras.layers.Dense layers, specified by n_nodes. Each dense layer transforms the input variable as:

\[ \begin{aligned} \mathbb{X}'_{\ldots{}\beta} &= \mathrm{Dense}(\mathbb{X}_{\ldots{}\alpha}) \\ &= h\left( \sum_\alpha W_{\alpha\beta} \mathbb{X}_{\ldots{}\alpha} + b_{\beta} \right) \end{aligned} \]

, where \(W_{\alpha\beta}\), \(b_{\beta}\) are the learnable weights and biases, \(h\) is the activation function, and \(\mathbb{X}\) can be \(\mathbb{P}_{i\alpha}\) or \(\mathbb{I}_{ij\alpha}\) with \(\alpha,\beta\) being the indices of input/output channels. The keyward arguments are parsed into the class, which can be used to specify the bias, activation function, etc for the dense layer. FFLayer outputs a tensor with the shape [..., n_nodes[-1]].

In the PiNet architecture, PPLayer and IILayer are both instances of the FFLayer class , namely:

\[ \begin{aligned} \mathbb{I}_{ij\gamma} &= \mathrm{IILayer}(\mathbb{I}'_{ij\beta}) = \mathrm{FFLayer}(\mathbb{I}'_{ij\beta}) \\ \mathbb{P}_{i\delta} &= \mathrm{PPLayer}(\mathbb{P}''_{i\gamma}) = \mathrm{FFLayer}(\mathbb{P}'_{i\gamma}) \end{aligned} \]

, with the difference that IILayers have their baises set to zero to avoid discontinuity in the model output.

Source code in pinn/networks/pinet.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class FFLayer(tf.keras.layers.Layer):
    R"""`FFLayer` is a shortcut to create a multi-layer perceptron (MLP) or a
    feed-forward network. A `FFLayer` takes one tensor as input of arbitratry
    shape, and parse it to a list of `tf.keras.layers.Dense` layers, specified
    by `n_nodes`. Each dense layer transforms the input variable as:

    $$
    \begin{aligned}
    \mathbb{X}'_{\ldots{}\beta} &= \mathrm{Dense}(\mathbb{X}_{\ldots{}\alpha}) \\
      &= h\left( \sum_\alpha W_{\alpha\beta} \mathbb{X}_{\ldots{}\alpha} + b_{\beta} \right)
    \end{aligned}
    $$

    , where $W_{\alpha\beta}$, $b_{\beta}$ are the learnable weights and biases,
    $h$ is the activation function, and $\mathbb{X}$ can be
    $\mathbb{P}_{i\alpha}$ or $\mathbb{I}_{ij\alpha}$ with $\alpha,\beta$ being
    the indices of input/output channels. The keyward arguments are parsed into
    the class, which can be used to specify the bias, activation function, etc
    for the dense layer. `FFLayer` outputs a tensor with the shape `[...,
    n_nodes[-1]]`.


    In the PiNet architecture, `PPLayer` and `IILayer` are both instances of the
    `FFLayer` class , namely:

    $$
    \begin{aligned}
      \mathbb{I}_{ij\gamma} &= \mathrm{IILayer}(\mathbb{I}'_{ij\beta}) = \mathrm{FFLayer}(\mathbb{I}'_{ij\beta}) \\
      \mathbb{P}_{i\delta} &= \mathrm{PPLayer}(\mathbb{P}''_{i\gamma}) = \mathrm{FFLayer}(\mathbb{P}'_{i\gamma})
    \end{aligned}
    $$

    , with the difference that `IILayer`s have their baises set to zero to avoid
    discontinuity in the model output.

    """

    def __init__(self, n_nodes=[64, 64], **kwargs):
        """
        Args:
            n_nodes (list): dimension of the layers
            **kwargs (dict): options to be parsed to dense layers
        """
        super(FFLayer, self).__init__()
        self.dense_layers = [
            tf.keras.layers.Dense(n_node, **kwargs) for n_node in n_nodes
        ]

    def call(self, tensor):
        """
        Args:
            tensor (tensor): input tensor

        Returns:
            tensor (tensor): tensor with shape `(...,n_nodes[-1])`
        """
        for layer in self.dense_layers:
            tensor = layer(tensor)
        return tensor

__init__(n_nodes=[64, 64], **kwargs)

Parameters:

Name Type Description Default
n_nodes list

dimension of the layers

[64, 64]
**kwargs dict

options to be parsed to dense layers

{}
Source code in pinn/networks/pinet.py
52
53
54
55
56
57
58
59
60
61
def __init__(self, n_nodes=[64, 64], **kwargs):
    """
    Args:
        n_nodes (list): dimension of the layers
        **kwargs (dict): options to be parsed to dense layers
    """
    super(FFLayer, self).__init__()
    self.dense_layers = [
        tf.keras.layers.Dense(n_node, **kwargs) for n_node in n_nodes
    ]

call(tensor)

Parameters:

Name Type Description Default
tensor tensor

input tensor

required

Returns:

Name Type Description
tensor tensor

tensor with shape (...,n_nodes[-1])

Source code in pinn/networks/pinet.py
63
64
65
66
67
68
69
70
71
72
73
def call(self, tensor):
    """
    Args:
        tensor (tensor): input tensor

    Returns:
        tensor (tensor): tensor with shape `(...,n_nodes[-1])`
    """
    for layer in self.dense_layers:
        tensor = layer(tensor)
    return tensor

pinet.PILayer

Bases: Layer

PILayer takes the properties (\(\mathbb{P}_{i\alpha}, \mathbb{P}_{j\alpha}\)) of a pair of atoms as input and outputs a set of interactions for each pair. The inputs will be broadcasted and concatenated as the input of a feed-forward neural network (FFLayer), and the interactions are generated by taking the output of the FFLayer as weights of radial basis functions, i.e.:

\[ \begin{aligned} w_{ij(b\beta)} &= \mathrm{FFLayer}\left((\mathbf{1}_{j}\mathbb{P}_{i\alpha})\Vert(\mathbf{1}_{i}\mathbb{P}_{j\alpha})\right) \\ \mathbb{I}'_{ij\beta} &= \sum_b W_{ij(b\beta)} \, e_{ijb} \end{aligned} \]

, where \(w_{ij(b\beta)}\) is an intemediate weight tensor for the radial basis functions, output by the FFLayer; the output channel is reshaped into two dimensions, where \(b\) is the index for the basis function and \(d\) is the index for output interaction.

n_nodes specifies the number of nodes in the FFLayer. Note that the last element of n_nodes specifies the number of output channels after applying the basis function (\(d\) instead of \(bd\)), i.e. the output dimension of FFLayer is [n_pairs,n_nodes[-1]*n_basis], the output is then summed with the basis to form the output interaction.

Source code in pinn/networks/pinet.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
class PILayer(tf.keras.layers.Layer):
    R"""`PILayer` takes the properties ($\mathbb{P}_{i\alpha},
    \mathbb{P}_{j\alpha}$) of a pair of atoms as input and outputs a set of
    interactions for each pair. The inputs will be broadcasted and concatenated
    as the input of a feed-forward neural network (`FFLayer`), and the
    interactions are generated by taking the output of the `FFLayer` as weights
    of radial basis functions, i.e.:

    $$
    \begin{aligned}
    w_{ij(b\beta)} &= \mathrm{FFLayer}\left((\mathbf{1}_{j}\mathbb{P}_{i\alpha})\Vert(\mathbf{1}_{i}\mathbb{P}_{j\alpha})\right) \\
    \mathbb{I}'_{ij\beta} &= \sum_b W_{ij(b\beta)} \, e_{ijb}
    \end{aligned}
    $$

    , where $w_{ij(b\beta)}$ is an intemediate weight tensor for the
    radial basis functions, output by the `FFLayer`; the output channel is
    reshaped into two dimensions, where $b$ is the index for the basis function
    and $d$ is the index for output interaction.


    `n_nodes` specifies the number of nodes in the `FFLayer`. Note that the last
    element of n_nodes specifies the number of output channels after applying
    the basis function ($d$ instead of $bd$), i.e. the output dimension of
    FFLayer is `[n_pairs,n_nodes[-1]*n_basis]`, the output is then summed with
    the basis to form the output interaction.

    """

    def __init__(self, n_nodes=[64], **kwargs):
        """
        Args:
            n_nodes (list of int): number of nodes to use
            **kwargs (dict): keyword arguments will be parsed to the feed forward layers
        """
        super(PILayer, self).__init__()
        self.n_nodes = n_nodes
        self.kwargs = kwargs

    def build(self, shapes):
        """"""
        self.n_basis = shapes[2][-1]
        n_nodes_iter = self.n_nodes.copy()
        n_nodes_iter[-1] *= self.n_basis
        self.ff_layer = FFLayer(n_nodes_iter, **self.kwargs)

    def call(self, tensors):
        """
        PILayer take a list of three tensors as input:

        - ind_2: [sparse indices](layers.md#sparse-indices) of pairs with shape `(n_pairs, 2)`
        - prop: property tensor with shape `(n_atoms, n_prop)`
        - basis: interaction tensor with shape `(n_pairs, n_basis)`

        Args:
            tensors (list of tensors): list of `[ind_2, prop, basis]` tensors

        Returns:
            inter (tensor): interaction tensor with shape `(n_pairs, n_nodes[-1])`
        """
        ind_2, prop, basis = tensors
        ind_i = ind_2[:, 0]
        ind_j = ind_2[:, 1]
        prop_i = tf.gather(prop, ind_i)
        prop_j = tf.gather(prop, ind_j)

        inter = tf.concat([prop_i, prop_j], axis=-1)
        inter = self.ff_layer(inter)
        inter = tf.reshape(inter, [-1, self.n_nodes[-1], self.n_basis])
        inter = tf.einsum("pcb,pb->pc", inter, basis)
        return inter

__init__(n_nodes=[64], **kwargs)

Parameters:

Name Type Description Default
n_nodes list of int

number of nodes to use

[64]
**kwargs dict

keyword arguments will be parsed to the feed forward layers

{}
Source code in pinn/networks/pinet.py
105
106
107
108
109
110
111
112
113
def __init__(self, n_nodes=[64], **kwargs):
    """
    Args:
        n_nodes (list of int): number of nodes to use
        **kwargs (dict): keyword arguments will be parsed to the feed forward layers
    """
    super(PILayer, self).__init__()
    self.n_nodes = n_nodes
    self.kwargs = kwargs

build(shapes)

Source code in pinn/networks/pinet.py
115
116
117
118
119
120
def build(self, shapes):
    """"""
    self.n_basis = shapes[2][-1]
    n_nodes_iter = self.n_nodes.copy()
    n_nodes_iter[-1] *= self.n_basis
    self.ff_layer = FFLayer(n_nodes_iter, **self.kwargs)

call(tensors)

PILayer take a list of three tensors as input:

  • ind_2: sparse indices of pairs with shape (n_pairs, 2)
  • prop: property tensor with shape (n_atoms, n_prop)
  • basis: interaction tensor with shape (n_pairs, n_basis)

Parameters:

Name Type Description Default
tensors list of tensors

list of [ind_2, prop, basis] tensors

required

Returns:

Name Type Description
inter tensor

interaction tensor with shape (n_pairs, n_nodes[-1])

Source code in pinn/networks/pinet.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def call(self, tensors):
    """
    PILayer take a list of three tensors as input:

    - ind_2: [sparse indices](layers.md#sparse-indices) of pairs with shape `(n_pairs, 2)`
    - prop: property tensor with shape `(n_atoms, n_prop)`
    - basis: interaction tensor with shape `(n_pairs, n_basis)`

    Args:
        tensors (list of tensors): list of `[ind_2, prop, basis]` tensors

    Returns:
        inter (tensor): interaction tensor with shape `(n_pairs, n_nodes[-1])`
    """
    ind_2, prop, basis = tensors
    ind_i = ind_2[:, 0]
    ind_j = ind_2[:, 1]
    prop_i = tf.gather(prop, ind_i)
    prop_j = tf.gather(prop, ind_j)

    inter = tf.concat([prop_i, prop_j], axis=-1)
    inter = self.ff_layer(inter)
    inter = tf.reshape(inter, [-1, self.n_nodes[-1], self.n_basis])
    inter = tf.einsum("pcb,pb->pc", inter, basis)
    return inter

pinet.IPLayer

Bases: Layer

The IPLayer transforms pairwise interactions to atomic properties

The IPLayer has no learnable variables and simply sums up the pairwise interations. Thus the returned property has the same shape with the input interaction, i.e.:

\[ \begin{aligned} \mathbb{P}_{i\gamma} = \mathrm{IPLayer}(\mathbb{I}_{ij\gamma}) = \sum_{j} \mathbb{I}_{ij\gamma} \end{aligned} \]
Source code in pinn/networks/pinet.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
class IPLayer(tf.keras.layers.Layer):
    R"""The IPLayer transforms pairwise interactions to atomic properties

    The IPLayer has no learnable variables and simply sums up the pairwise
    interations. Thus the returned property has the same shape with the
    input interaction, i.e.:

    $$
    \begin{aligned}
    \mathbb{P}_{i\gamma} = \mathrm{IPLayer}(\mathbb{I}_{ij\gamma}) = \sum_{j} \mathbb{I}_{ij\gamma}
    \end{aligned}
    $$

    """

    def __init__(self):
        """
        IPLayer does not require any parameter, initialize as `IPLayer()`.
        """
        super(IPLayer, self).__init__()

    def call(self, tensors):
        """
        IPLayer take a list of three tensors list as input:

        - ind_2: [sparse indices](layers.md#sparse-indices) of pairs with shape `(n_pairs, 2)`
        - prop: property tensor with shape `(n_atoms, n_prop)`
        - inter: interaction tensor with shape `(n_pairs, n_inter)`

        Args:
            tensors (list of tensor): list of [ind_2, prop, inter] tensors

        Returns:
            prop (tensor): new property tensor with shape `(n_atoms, n_inter)`
        """
        ind_2, prop, inter = tensors
        n_atoms = tf.shape(prop)[0]
        return tf.math.unsorted_segment_sum(inter, ind_2[:, 0], n_atoms)

__init__()

IPLayer does not require any parameter, initialize as IPLayer().

Source code in pinn/networks/pinet.py
164
165
166
167
168
def __init__(self):
    """
    IPLayer does not require any parameter, initialize as `IPLayer()`.
    """
    super(IPLayer, self).__init__()

call(tensors)

IPLayer take a list of three tensors list as input:

  • ind_2: sparse indices of pairs with shape (n_pairs, 2)
  • prop: property tensor with shape (n_atoms, n_prop)
  • inter: interaction tensor with shape (n_pairs, n_inter)

Parameters:

Name Type Description Default
tensors list of tensor

list of [ind_2, prop, inter] tensors

required

Returns:

Name Type Description
prop tensor

new property tensor with shape (n_atoms, n_inter)

Source code in pinn/networks/pinet.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def call(self, tensors):
    """
    IPLayer take a list of three tensors list as input:

    - ind_2: [sparse indices](layers.md#sparse-indices) of pairs with shape `(n_pairs, 2)`
    - prop: property tensor with shape `(n_atoms, n_prop)`
    - inter: interaction tensor with shape `(n_pairs, n_inter)`

    Args:
        tensors (list of tensor): list of [ind_2, prop, inter] tensors

    Returns:
        prop (tensor): new property tensor with shape `(n_atoms, n_inter)`
    """
    ind_2, prop, inter = tensors
    n_atoms = tf.shape(prop)[0]
    return tf.math.unsorted_segment_sum(inter, ind_2[:, 0], n_atoms)

pinet.ResUpdate

Bases: Layer

ResUpdate layer implements ResNet-like update of properties that addresses vanishing/exploding gradient problems (see arXiv:1512.03385).

It takes two tensors (old and new) as input, the tensors should have the same shape except for the last dimension, and a tensor with the shape of the new tensor is always returned.

If shapes of the two tensors match, their sum is returned. If the two tensors' shapes differ in the last dimension, the old tensor will be added to the new after a learnable linear transformation that matches its shape to the new tensor, i.e., according to the above flowchart:

\[ \begin{aligned} \mathbb{P}'_{i\gamma} &= \mathrm{ResUpdate}(\mathbb{P}^{t}_{i\alpha},\mathbb{P}''_{i\gamma}) & \\ &= \begin{cases} \mathbb{P}^{t}_{i\alpha} + \mathbb{P}''_{i\gamma} & \textrm{, if } \mathrm{dim}(\mathbb{P}^{t}) = \mathrm{dim}(\mathbb{P}'')\\ \sum_{\alpha} W_{\alpha\gamma} \, \mathbb{P}^{t}_{i\alpha} + \mathbb{P}''_{i\gamma} & \textrm{, if } \mathrm{dim}(\mathbb{P}^{t}) \ne \mathrm{dim}(\mathbb{P}'') \end{cases} \end{aligned} \]

, where \(W_{\alpha\beta}\) is a learnable weight matrix if needed.

In the PiNet architecture above, ResUpdate is only used to update the properties after the IPLayer, when ii_nodes[-1]==pp_nodes[-1], the weight matrix is only necessary at \(t=0\).

Source code in pinn/networks/pinet.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
class ResUpdate(tf.keras.layers.Layer):
    R"""`ResUpdate` layer implements ResNet-like update of properties that
    addresses vanishing/exploding gradient problems (see
    [arXiv:1512.03385](https://arxiv.org/abs/1512.03385)).

    It takes two tensors (old and new) as input, the tensors should have the
    same shape except for the last dimension, and a tensor with the shape of the
    new tensor is always returned.

    If shapes of the two tensors match, their sum is returned. If the two
    tensors' shapes differ in the last dimension, the old tensor will be added
    to the new after a learnable linear transformation that matches its shape to
    the new tensor, i.e., according to the above flowchart:

    $$
    \begin{aligned}
    \mathbb{P}'_{i\gamma} &= \mathrm{ResUpdate}(\mathbb{P}^{t}_{i\alpha},\mathbb{P}''_{i\gamma}) & \\
      &= \begin{cases}
           \mathbb{P}^{t}_{i\alpha} + \mathbb{P}''_{i\gamma} & \textrm{, if } \mathrm{dim}(\mathbb{P}^{t}) = \mathrm{dim}(\mathbb{P}'')\\
           \sum_{\alpha} W_{\alpha\gamma} \, \mathbb{P}^{t}_{i\alpha} + \mathbb{P}''_{i\gamma} & \textrm{, if } \mathrm{dim}(\mathbb{P}^{t}) \ne \mathrm{dim}(\mathbb{P}'')
         \end{cases}
    \end{aligned}
    $$

    , where $W_{\alpha\beta}$ is a learnable weight matrix if needed.

    In the PiNet architecture above, ResUpdate is only used to update the
    properties after the `IPLayer`, when `ii_nodes[-1]==pp_nodes[-1]`, the
    weight matrix is only necessary at $t=0$.
    """

    def __init__(self):
        """
        ResUpdate does not require any parameter, initialize as `ResUpdate()`.
        """
        super(ResUpdate, self).__init__()

    def build(self, shapes):
        """"""
        assert isinstance(shapes, list) and len(shapes) == 2
        if shapes[0][-1] == shapes[1][-1]:
            self.transform = lambda x: x
        else:
            self.transform = tf.keras.layers.Dense(
                shapes[1][-1], use_bias=False, activation=None
            )

    def call(self, tensors):
        """
        Args:
           tensors (list of tensors): two tensors with matching shapes expect the last dimension

        Returns:
           tensor (tensor): updated tensor with the same shape as the second input tensor
        """
        old, new = tensors
        return self.transform(old) + new

__init__()

ResUpdate does not require any parameter, initialize as ResUpdate().

Source code in pinn/networks/pinet.py
281
282
283
284
285
def __init__(self):
    """
    ResUpdate does not require any parameter, initialize as `ResUpdate()`.
    """
    super(ResUpdate, self).__init__()

build(shapes)

Source code in pinn/networks/pinet.py
287
288
289
290
291
292
293
294
295
def build(self, shapes):
    """"""
    assert isinstance(shapes, list) and len(shapes) == 2
    if shapes[0][-1] == shapes[1][-1]:
        self.transform = lambda x: x
    else:
        self.transform = tf.keras.layers.Dense(
            shapes[1][-1], use_bias=False, activation=None
        )

call(tensors)

Parameters:

Name Type Description Default
tensors list of tensors

two tensors with matching shapes expect the last dimension

required

Returns:

Name Type Description
tensor tensor

updated tensor with the same shape as the second input tensor

Source code in pinn/networks/pinet.py
297
298
299
300
301
302
303
304
305
306
def call(self, tensors):
    """
    Args:
       tensors (list of tensors): two tensors with matching shapes expect the last dimension

    Returns:
       tensor (tensor): updated tensor with the same shape as the second input tensor
    """
    old, new = tensors
    return self.transform(old) + new

pinet.OutLayer

Bases: Layer

OutLayer updates the network output with a FFLayer layer, where the out_units controls the dimension of outputs. In addition to the FFLayer specified by n_nodes, the OutLayer has one additional linear biasless layer that scales the outputs, specified by out_units.

Source code in pinn/networks/pinet.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
class OutLayer(tf.keras.layers.Layer):
    """`OutLayer` updates the network output with a `FFLayer` layer, where the
    `out_units` controls the dimension of outputs. In addition to the `FFLayer`
    specified by `n_nodes`, the `OutLayer` has one additional linear biasless
    layer that scales the outputs, specified by `out_units`.

    """

    def __init__(self, n_nodes, out_units, **kwargs):
        """
        Args:
            n_nodes (list): dimension of the hidden layers
            out_units (int): dimension of the output units
            **kwargs (dict): options to be parsed to dense layers
        """
        super(OutLayer, self).__init__()
        self.out_units = out_units
        self.ff_layer = FFLayer(n_nodes, **kwargs)
        self.out_units = tf.keras.layers.Dense(
            out_units, activation=None, use_bias=False
        )

    def call(self, tensors):
        """
        OutLayer takes a list of three tensors as input:

        - ind_1: [sparse indices](layers.md#sparse-indices) of atoms with shape `(n_atoms, 2)`
        - prop: property tensor with shape `(n_atoms, n_prop)`
        - prev_output:  previous output with shape `(n_atoms, out_units)`

        Args:
            tensors (list of tensors): list of [ind_1, prop, prev_output] tensors

        Returns:
            output (tensor): an updated output tensor with shape `(n_atoms, out_units)`
        """
        ind_1, prop, prev_output = tensors
        prop = self.ff_layer(prop)
        output = self.out_units(prop) + prev_output
        return output

__init__(n_nodes, out_units, **kwargs)

Parameters:

Name Type Description Default
n_nodes list

dimension of the hidden layers

required
out_units int

dimension of the output units

required
**kwargs dict

options to be parsed to dense layers

{}
Source code in pinn/networks/pinet.py
197
198
199
200
201
202
203
204
205
206
207
208
209
def __init__(self, n_nodes, out_units, **kwargs):
    """
    Args:
        n_nodes (list): dimension of the hidden layers
        out_units (int): dimension of the output units
        **kwargs (dict): options to be parsed to dense layers
    """
    super(OutLayer, self).__init__()
    self.out_units = out_units
    self.ff_layer = FFLayer(n_nodes, **kwargs)
    self.out_units = tf.keras.layers.Dense(
        out_units, activation=None, use_bias=False
    )

call(tensors)

OutLayer takes a list of three tensors as input:

  • ind_1: sparse indices of atoms with shape (n_atoms, 2)
  • prop: property tensor with shape (n_atoms, n_prop)
  • prev_output: previous output with shape (n_atoms, out_units)

Parameters:

Name Type Description Default
tensors list of tensors

list of [ind_1, prop, prev_output] tensors

required

Returns:

Name Type Description
output tensor

an updated output tensor with shape (n_atoms, out_units)

Source code in pinn/networks/pinet.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def call(self, tensors):
    """
    OutLayer takes a list of three tensors as input:

    - ind_1: [sparse indices](layers.md#sparse-indices) of atoms with shape `(n_atoms, 2)`
    - prop: property tensor with shape `(n_atoms, n_prop)`
    - prev_output:  previous output with shape `(n_atoms, out_units)`

    Args:
        tensors (list of tensors): list of [ind_1, prop, prev_output] tensors

    Returns:
        output (tensor): an updated output tensor with shape `(n_atoms, out_units)`
    """
    ind_1, prop, prev_output = tensors
    prop = self.ff_layer(prop)
    output = self.out_units(prop) + prev_output
    return output

  1. 1 Y. Shao, M. Hellström, P.D. Mitev, L. Knijff, and C. Zhang, “PiNN: A python library for building atomic neural networks of molecules and materials,” J. Chem. Inf. Model. 60(3), 1184–1193 (2020). 

« Previous
Next »