A node is a discrete functional unit that is called by the interpreter at run time when the corresponding node in the graph file is processed. Nodes are written in Python and stored in Python modules. Node modules are collected together into Ana packages and stored in the appropriate Ana package directory, e.g. <channel-root>/packages/<package-name>/<package-name>/nodes/<node-module>.py

Nodes are Python classes that are derived from an abstract base class called “Node”. The Node base class is implemented in the anatools.lib.node module.

Here is a simple node definition:

from anatools.lib.node import Node

class MyNode(Node):
    def exec(self):
        return {}

In this example, the derived node class is called “MyNode”. Note that the Node base class implements an abstract public member function called “exec” that is called by the interpreter when it is executed. The derived class overrides this functions in order to implement its execution logic. The exec function always returns a dictionary which is the output of the node. In this case the exec function performs no function and returns an empty dictionary.

A node in a graph can receive input and produce output. This is done via input and output ports. Nodes implement ports as dictionaries.

Here is an example of a node that inputs two values, adds them together, and outputs the sum:

from anatools.lib.node import Node

class Add(Node):
    def exec(self):
        value1 = float(self.inputs["Value1"][0])
        value2 = float(self.inputs["Value2"][0])
        sum = value1 + value2
        return {"Sum": sum}

Input ports are implemented via the “inputs” dictionary which is accessible as an instance variable of the node. The dictionary key is the name of the input port. In this example, we have two input ports named “Value1” and “Value2”.

Since input ports can have multiple incoming links, the incoming values are stored in a list. In this example, we assume both input ports have a single incoming value so we use the zero index for each input port.

Note that input variables should be explicitly converted to the expected data type (in this case float). This is because fixed input values entered into the GUI are stored and returned as text strings. Note that data type conversion errors may need to be accounted for.

Output port data is returned as a dictionary. Each entry in the dictionary corresponds to one of the node output ports. In the above example, the sum of the two input values is returned as output port “Sum”.

Nodes should perform error checking. If there is an unrecoverable error then a message should be generated and execution terminated. Here is an example of error checking in a node:

import sys
import logging

logger = logging.getLogger(__name__)

class OnlyInteger(Node):
    def exec(self):
            an_int = int(self.inputs["an_int"][0])
        except ValueError as e:
            logging.error("Error converting port 'an_int' to type int", exc_info=e)

Ana uses the standard Python logging facility for error messages. In this example, the input data type conversion is surrounded by a try/except. If a ValueError occurs then a message is logged and the application exits.

The default logging level is set to “ERROR”. When running Ana from the command line, this can be changed at runtime via the “--loglevel” switch.

When the interpreter is run interactively, error messages are printed to the console. Messages can also be optionally written to a log file via the “--logfile” command line switch. When the application is run in the cloud, ERROR and higher level messages are displayed in the web interface.

If an error occurs in a node and it is not caught then it will be caught by a last chance handler in the interpreter. In that case, an ERROR level message will be printed and execution will terminate.