Tutorial 10 - Simple Data Node in Python

The simple data node creates one input attribute and one output attribute of each of the simple types, where “simple” refers to data types that have a single component and are not arrays. (e.g. “float” is simple, “float[3]” is not, nor is “float[]”). See also Tutorial 2 - Simple Data Node for a similar example in C++.

Automatic Python Node Registration

By implementing the standard Carbonite extension interfact in Python, OmniGraph will know to scan your Python import path for to recursively scan the directory, import all Python node files it finds, and register those nodes. It will also deregister those nodes when the extension shuts down. Here is an example of the directory structure for an extension with a single node in it. (For extensions that have a premake5.lua build script this will be in the build directory. For standalone extensions it is in your source directory.)

omni.my.extension/
    omni/
        my/
            extension/
                nodes/
                    OgnMyNode.ogn
                    OgnMyNode.py

OgnTutorialSimpleDataPy.ogn

The ogn file shows the implementation of a node named “omni.graph.tutorials.SimpleDataPy”, which has one input and one output attribute of each simple type.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 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
 74
 75
 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
147
148
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
187
188
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
{
    "SimpleDataPy" : {
        "version": 1,
        "categories": "tutorials",
        "description": [
            "This is a tutorial node. It creates both an input and output attribute of every simple ",
            "supported data type. The values are modified in a simple way so that the compute modifies values. ",
            "It is the same as node omni.graph.tutorials.SimpleData, except it is implemented in Python instead of C++."
        ],
        "language": "python",
        "$iconOverride": [
            "If for some reason the default name or colors for the icon are inappropriate you can override like this.",
            "This gives an alternative icon path, a shape color of Green, a border color of Red, and a background",
            "color of half-opaque Blue. If you just want to override the icon path then instead of a dictionary you",
            "can just use the string path, as in \"icon\": \"Tutorial10Icon.svg\"."
        ],
        "icon": {
            "path": "Tutorial10Icon.svg",
            "color": "#FF00FF00",
            "borderColor": [255, 0, 0, 255],
            "backgroundColor": "#7FFF0000"
        },
        "metadata":
        {
           "uiName": "Tutorial Python Node: Attributes With Simple Data"
        },
        "inputs": {
            "a_bool": {
                "type": "bool",
                "metadata": {
                    "uiName": "Simple Boolean Input"
                },
                "description": ["This is an attribute of type boolean"],
                "default": true,
                "$optional": "When this is set there is no checking for validity before calling compute()",
                "optional": true
            },
            "a_half": {
                "type": "half",
                "description": ["This is an attribute of type 16 bit float"],
                "$comment": "0 is used as the decimal portion due to reduced precision of this type",
                "default": 0.0
            },
            "a_int": {
                "type": "int",
                "description": ["This is an attribute of type 32 bit integer"],
                "default": 0
            },
            "a_int64": {
                "type": "int64",
                "description": ["This is an attribute of type 64 bit integer"],
                "default": 0
            },
            "a_float": {
                "type": "float",
                "description": ["This is an attribute of type 32 bit floating point"],
                "default": 0
            },
            "a_double": {
                "type": "double",
                "description": ["This is an attribute of type 64 bit floating point"],
                "default": 0
            },
            "a_path": {
                "type": "path",
                "description": ["This is an attribute of type path"],
                "default": ""
            },
            "a_string": {
                "type": "string",
                "description": ["This is an attribute of type string"],
                "default": "helloString"
            },
             "a_token": {
                "type": "token",
                "description": ["This is an attribute of type interned string with fast comparison and hashing"],
                "default": "helloToken"
            },
            "a_objectId": {
                "type": "objectId",
                "description": ["This is an attribute of type objectId"],
                "default": 0
            },
            "a_uchar": {
               "type": "uchar",
               "description": ["This is an attribute of type unsigned 8 bit integer"],
               "default": 0
            },
            "a_uint": {
               "type": "uint",
               "description": ["This is an attribute of type unsigned 32 bit integer"],
               "default": 0
            },
            "a_uint64": {
                "type": "uint64",
                "description": ["This is an attribute of type unsigned 64 bit integer"],
                "default": 0
             },
             "a_constant_input": {
                 "type": "int",
                 "description": ["This is an input attribute whose value can be set but can only be connected as a source."],
                 "metadata": {
                     "outputOnly": "1"
                 }
             }
        },
        "outputs": {
            "a_bool": {
                "type": "bool",
                "description": ["This is a computed attribute of type boolean"]
            },
            "a_half": {
                "type": "half",
                "description": ["This is a computed attribute of type 16 bit float"]
            },
            "a_int": {
                "type": "int",
                "description": ["This is a computed attribute of type 32 bit integer"]
            },
            "a_int64": {
                "type": "int64",
                "description": ["This is a computed attribute of type 64 bit integer"]
            },
            "a_float": {
                "type": "float",
                "description": ["This is a computed attribute of type 32 bit floating point"]
            },
            "a_double": {
                "type": "double",
                "description": ["This is a computed attribute of type 64 bit floating point"]
            },
            "a_path": {
                "type": "path",
                "description": ["This is a computed attribute of type path"],
                "default": "/Child"
            },
            "a_string": {
                "type": "string",
                "description": ["This is a computed attribute of type string"],
                "default": "This string is empty"
            },
            "a_token": {
               "type": "token",
               "description": ["This is a computed attribute of type interned string with fast comparison and hashing"]
            },
            "a_objectId": {
                "type": "objectId",
                "description": ["This is a computed attribute of type objectId"]
            },
            "a_uchar": {
               "type": "uchar",
               "description": ["This is a computed attribute of type unsigned 8 bit integer"]
            },
            "a_uint": {
               "type": "uint",
               "description": ["This is a computed attribute of type unsigned 32 bit integer"]
            },
            "a_uint64": {
                "type": "uint64",
                "description": ["This is a computed attribute of type unsigned 64 bit integer"]
            },
            "a_nodeTypeUiName": {
                "type": "string",
                "description": "Computed attribute containing the UI name of this node type"
            },
            "a_a_boolUiName": {
                "type": "string",
                "description": "Computed attribute containing the UI name of input a_bool"
            }
        },
        "tests": [
            {
                "$comment": ["Each test has a description of the test and a set of input and output values. ",
                             "The test runs by setting all of the specified inputs on the node to their values, ",
                             "running the compute, then comparing the computed outputs against the values ",
                             "specified in the test. Only the inputs in the list are set; others will use their ",
                             "default values. Only the outputs in the list are checked; others are ignored."],
                "description": "Check that false becomes true",
                "inputs:a_bool": false,
                "outputs:a_bool": true
            },
            {
                "$comment": "This is a more verbose format of test data that provides a different grouping of values",
                "description": "Check that true becomes false",
                "inputs": {
                    "a_bool": true
                },
                "outputs": {
                    "a_bool": false,
                    "a_a_boolUiName": "Simple Boolean Input",
                    "a_nodeTypeUiName": "Tutorial Python Node: Attributes With Simple Data"
                }
            },
            {
                "$comment": "Make sure the path append does the right thing",
                "inputs:a_path": "/World/Domination", "outputs:a_path": "/World/Domination/Child"
            },
            {
                "$comment": "Even though these computations are all independent they can be checked in a single test.",
                "description": "Check all attributes against their computed values",
                "inputs:a_bool": false, "outputs:a_bool": true,
                "inputs:a_double": 1.1, "outputs:a_double": 2.1,
                "inputs:a_float": 3.3, "outputs:a_float": 4.3,
                "inputs:a_half": 5.0, "outputs:a_half": 6.0,
                "inputs:a_int": 7, "outputs:a_int": 8,
                "inputs:a_int64": 9, "outputs:a_int64": 10,
                "inputs:a_token": "helloToken", "outputs:a_token": "worldToken",
                "inputs:a_string": "helloString", "outputs:a_string": "worldString",
                "inputs:a_objectId": 10, "outputs:a_objectId": 11,
                "inputs:a_uchar": 11, "outputs:a_uchar": 12,
                "inputs:a_uint": 13, "outputs:a_uint": 14,
                "inputs:a_uint64": 15, "outputs:a_uint64": 16
            }
        ]
    }
}

OgnTutorialSimpleDataPy.py

The py file contains the implementation of the compute method, which modifies each of the inputs in a simple way to create outputs that have different values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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
"""
Implementation of the Python node accessing all of the simple data types.
This class exercises access to the DataModel through the generated database class for all simple data types.
It implements the same algorithm as the C++ node OgnTutorialSimpleData.cpp
"""
import omni.graph.tools.ogn as ogn


class OgnTutorialSimpleDataPy:
    """Exercise the simple data types through a Python OmniGraph node"""

    @staticmethod
    def compute(db) -> bool:
        """Perform a trivial computation on all of the simple data types to make testing easy"""
        # Inside the database the contained object "inputs" holds the data references for all input attributes and the
        # contained object "outputs" holds the data references for all output attributes.

        # Each of the attribute accessors are named for the name of the attribute, with the ":" replaced by "_".
        # The colon is used in USD as a convention for creating namespaces so it's safe to replace it without
        # modifying the meaning. The "inputs:" and "outputs:" prefixes in the generated attributes are matched
        # by the container names.

        # For example attribute "inputs:translate:x" would be accessible as "db.inputs.translate_x" and attribute
        # "outputs:matrix" would be accessible as "db.outputs.matrix".

        # The "compute" of this method modifies each attribute in a subtle way so that a test can be written
        # to verify the operation of the node. See the .ogn file for a description of tests.
        db.outputs.a_bool = not db.inputs.a_bool
        db.outputs.a_half = 1.0 + db.inputs.a_half
        db.outputs.a_int = 1 + db.inputs.a_int
        db.outputs.a_int64 = 1 + db.inputs.a_int64
        db.outputs.a_double = 1.0 + db.inputs.a_double
        db.outputs.a_float = 1.0 + db.inputs.a_float
        db.outputs.a_uchar = 1 + db.inputs.a_uchar
        db.outputs.a_uint = 1 + db.inputs.a_uint
        db.outputs.a_uint64 = 1 + db.inputs.a_uint64
        db.outputs.a_string = db.inputs.a_string.replace("hello", "world")
        db.outputs.a_objectId = 1 + db.inputs.a_objectId

        # The token interface is made available in the database as well, for convenience.
        # By calling "db.token" you can look up the token ID of a given string.
        if db.inputs.a_token == "helloToken":
            db.outputs.a_token = "worldToken"

        # Path just gets a new child named "Child".
        # In the implementation the string is manipulated directly, as it does not care if the SdfPath is valid or
        # not. If you want to manipulate it using the pxr.Sdf.Path API this is how you could do it:
        #
        #     from pxr import Sdf
        #     input_path Sdf.Path(db.inputs.a_path)
        #     if input_path.IsValid():
        #         db.outputs.a_path() = input_path.AppendChild("/Child").GetString();
        #
        db.outputs.a_path = db.inputs.a_path + "/Child"

        # To access the metadata you have to go out to the ABI, though the hardcoded metadata tags are in the
        # OmniGraph Python namespace
        assert db.node.get_attribute("inputs:a_bool").get_metadata(ogn.MetadataKeys.UI_NAME) == "Simple Boolean Input"

        # You can also use the database interface to get the same data
        db.outputs.a_nodeTypeUiName = db.get_metadata(ogn.MetadataKeys.UI_NAME)
        db.outputs.a_a_boolUiName = db.get_metadata(ogn.MetadataKeys.UI_NAME, db.attributes.inputs.a_bool)

        return True

Note how the attribute values are available through the OgnTutorialSimpleDataPyDatabase class. The generated interface creates access methods for every attribute, named for the attribute itself. They are all implemented as Python properties, where inputs only have get methods and outputs have both get and set methods.

Pythonic Attribute Data

Three subsections are creating in the generated database class. The main section implements the node type ABI methods and uses introspection on your node class to call any versions of the ABI methods you have defined (see later tutorials for examples of how this works).

The other two subsections are classes containing attribute access properties for inputs and outputs. For naming consistency the class members are called inputs and outputs. For example, you can access the value of the input attribute named foo by referencing OgnTutorialSimpleDataPyDatabase.inputs.foo.

For the curious, here is an example of how the boolean attribute access is set up on the simple tutorial node:

class OgnTutorialSimpleDataPyDatabase:
    class InputAttributes:
        def __init__(self, context_helper, node):
            self.context_helper = context_helper
            self.node = node
            # A lock is used to prevent inputs from inadvertently being modified during a compute.
            # This is equivalent to the "const" approach used in C++.
            self.setting_locked = False
            # Caching the Attribute object lets future data access run faster
            self._a_bool = node.get_attribute('inputs:a_bool')
        @property
        def a_bool(self):
            return self.context_helper.get_attr_value(self._a_bool)
        @a_bool.setter
        def a_bool(self, value):
            if self.setting_locked:
                raise AttributeError('inputs:a_bool')
            self.context_helper.set_attr_value(value, self._a_bool)

    class OutputAttributes:
        def __init__(self, context_helper, node):
            self.context_helper = context_helper
            self.node = node
            self._a_bool = node.get_attribute('outputs:a_bool')
        @property
        def a_bool(self):
            return self.context_helper.get_attr_value(self._a_bool)
        @a_bool.setter
        def a_bool(self, value):
            self.context_helper.set_attr_value(value, self._a_bool)

    def __init__(self, context, node):
        self.context = context
        self.node = node
        self.context_helper = ContextHelper(context)
        self.inputs = OgnTutorialSimpleDataPyDatabase.InputAttributes(self.context_helper, node)
        self.outputs = OgnTutorialSimpleDataPyDatabase.OutputAttributes(self.context_helper, node)

Pythonic Attribute Access

In the USD file the attribute names are automatically namespaced as inputs:FOO or outputs:BAR. In the Python interface the colon is illegal so the contained classes above are used to make use of the dot-separated equivalent, as inputs.FOO or outputs.BAR.

While the underlying data types are stored in their exact form there is conversion when they are passed back to Python as Python has a more limited set of data types, though they all have compatible ranges. For this class, these are the types the properties provide:

Database Property

Returned Type

inputs.a_bool

bool

inputs.a_half

float

inputs.a_int

int

inputs.a_int64

int

inputs.a_float

float

inputs.a_double

float

inputs.a_token

str

outputs.a_bool

bool

outputs.a_half

float

outputs.a_int

int

outputs.a_int64

int

outputs.a_float

float

outputs.a_double

float

outputs.a_token

str

The data returned are all references to the real data in the FlatCache, our managed memory store, pointed to the correct location at evaluation time.

Python Helpers

A few helpers are provided in the database class definition to help make coding with it more natural.

Python logging

Two helper functions are providing in the database class to help provide more information when the compute method of a node has failed. Two methods are provided, both taking a formatted string describing the problem.

log_error(message) is used when the compute has run into some inconsistent or unexpected data, such as two input arrays that are supposed to have the same size but do not, like the normals and vertexes on a mesh.

log_warning(message) can be used when the compute has hit an unusual case but can still provide a consistent output for it, for example the deformation of an empty mesh would result in an empty mesh and a warning since that is not a typical use for the node.

Direct Pythonic ABI Access

All of the generated database classes provide access to the underlying INodeType ABI for those rare situations where you want to access the ABI directly. There are two members provided, which correspond to the objects passed in to the ABI compute method.

There is the graph evaluation context member, context: Py_GraphContext, for accessing the underlying OmniGraph evaluation context and its interface.

There is also the OmniGraph node member, node: Py_Node, for accessing the underlying OmniGraph node object and its interface.