TensorNodes allow parts of a model to be defined using TensorFlow and smoothly integrated with the rest of a Nengo model. TensorNodes work very similarly to a regular Node, except instead of executing arbitrary Python code they execute arbitrary TensorFlow code.

The TensorFlow code is defined in a function or callable class. This function accepts the current simulation time (if pass_time=True) and/or an input Tensor x (if node.shape_in is specified). x will have shape (sim.minibatch_size,) + node.shape_in, and the function should return a Tensor with shape (sim.minibatch_size,) + node.shape_out. node.shape_out will be inferred by calling the function once and checking the output, if it isn’t set when the Node is created.

def tensor_func(t, x):
    print(t)  # current simulation time
    print(x)  # input on current timestep

    return x + 1

my_node = nengo_dl.TensorNode(tensor_func, shape_in=(1,))

TensorNodes can also be used with Keras Layers, by passing an instantiated Layer to the TensorNode. Since Keras layers typically don’t take the simulation time as input, we can use the pass_time=False parameter to only pass x.

my_node = nengo_dl.TensorNode(tf.keras.layers.Dense(units=10),
                              shape_in=(1,), pass_time=False)

This also means that we can use custom Keras layers to implement more complicated TensorNode behaviour. For example, if a TensorNode requires internal parameter variables, those can be created inside a Layer’s build function.

class MyLayer(tf.keras.layers.Layer):
    def build(self, input_shapes):
        self.w = self.add_weight()

    def call(self, inputs):
        return inputs * self.w

my_node = nengo_dl.TensorNode(MyLayer(), shape_in=(1,), pass_time=False)

See the TensorFlow documentation for more details on creating custom Layers.

Once created, a TensorNode can then be used in a Nengo network just like any other Nengo object (for example, it can receive input from Connections or have its output recorded via Probes)

inp = nengo.Node(output=np.sin)
conn = nengo.Connection(inp, my_node)
probe = nengo.Probe(my_node)

NengoDL also provides another syntax for creating TensorNodes, designed for users more familiar with the Keras functional API. This is the Layer class. Under the hood, this is just a different way of creating TensorNodes, it simply combines the creation of a TensorNode and a Connection from some input object to that TensorNode in a single step.

For example, in Keras we would create a Layer like

x = tf.keras.Input(shape=(1,))
y = tf.keras.layers.Dense(units=10)(x)

The equivalent, using nengo_dl.Layer, would be

x = nengo.Node([0])
y = nengo_dl.Layer(tf.keras.layers.Dense(units=10))(x)

Which, under the hood, is equivalent to

x = nengo.Node([0])
y = nengo_dl.TensorNode(
    tf.keras.layers.Dense(units=10), pass_time=False, shape_in=(1,))
nengo.Connection(x, y, synapse=None)

See the TensorNode API for more details, or the examples below for demonstrations of using TensorNodes in practice.