OGN Code Samples - Python

This files contains a collection of examples for using the .ogn generated code from Python. There is no particular flow to these examples, they are used as reference data for the OGN User Guide.

In the examples below this import will be assumed when describing names from the OmniGraph API, in the spirit of common usage for packages such as numpy or pandas:

import omni.graph.core as og

Python Generated Database

When the .ogn files are processed and the implementation language is set to python it generates a database file through which all of the attribute data can be accessed. It also generates some utility functions that are useful in the context of a compute function. For the file OgnMyNode.ogn the database class will be named OgnMyNodeDatabase and can be imported directly from the generated ogn module inside your Python module.

from omni.examples.ogn.OgnMyNodeDatabase import OgnMyNodeDatabase as database

Usually you will not need to import the file though as the compute method is passed an instance to it. The contents of that database file will include these functions:

db.log_error("Explanation of error") # Log an error in the compute
db.log_warning("Explanation of warning") # Log a warning in the compute
db.log_warn("Explanation of warning") # An alias for log_warning
db.inputs # Object containing accessors for all input attribute data
db.outputs # Object containing accessors for all output attribute data
db.state # Object containing accessors for all state attribute data
database.per_node_internal_state(node) # Class method to get the internal state data attached to a specific node

The attribute members of db.inputs, db.outputs, and db.state are all properties. The input setter can only be used during node initialization.

Minimal Python Node Implementation

Every Python node must contain a node class definition with an implementation of the compute method that takes the database as a parameter and returns a boolean indicating if the compute succeeded. To enforce more stringent type checking on compute calls, import the database definition for the declaration.

# This line isn't strictly necessary. It's only useful for more stringent type information of the compute parameter.
# Note how the extra submodule "ogn" is appended to the extension's module to find the database file.
from ogn.examples.ogn.OgnNoOpDatabase import OgnNoOpDatabase
class OgnNoOp:
    @staticmethod
    def compute(db: OgnNoOpDatabase) -> bool:
        """This comment should describe the compute algorithm.
        Running help() on the database class will provide the information from the node type description field in the
        .ogn file. The information here should be a supplement to that, consisting of implementation notes.
        """
        # This logs a warning to the console, once
        db.log_warning("This node does nothing")

        # The node is accessible through the database. Normally you don't have to check its validity as it
        # will already be checked, it's just done here to illustrate access and error logging.
        if db.node is None or not db.node.isValid():
            # This logs an error to the console and should only be used for compute failure
            db.log_error("The node being computed is not valid");
            return False

        return True

Note

For simplicity, the import will be omitted from subsequent examples.

[C++ Version]

Python Node Type Metadata Access

When node types have metadata added to them they can be accessed through the Python bindings to the node ABI.

class OgnNodeMetadata:
    @staticmethod
    def compute(db) -> bool:
        # Specifically defined metadata can be accessed by name
        print(f"The author of this node is {db.get_metadata('author')}")

        # Some metadata is automatically added; you can see it by iterating over all of the existing metadata.
        # The Python iteration interfaces with the C++ ABI to make it seem like the metadata is an iterable list.
        for metadata_name, metadata_value in db.node.get_all_metadata():
            print(f"Metadata for {metadata_name} is {metadata_value}")

        return True

[C++ Version]

Python Node Icon Location Access

Specifying the icon location and color information creates consistently named pieces of metadata that the UI can use to present a more customized visual appearance.

class OgnNodeWithIcon:
    @staticmethod
    def compute(db) -> bool:
        # The icon path is just a special case of metadata. The hardcoded key is in the Python namespace
        path = db.get_metadata(og.MetadataKeys.ICON_PATH)
        color = db.get_metadata(og.MetadataKeys.ICON_COLOR)
        background_color = db.get_metadata(og.MetadataKeys.ICON_BACKGROUND_COLOR)
        border_color = db.get_metadata(og.MetadataKeys.ICON_BORDER_COLOR)
        if path is not None:
            print(f"Icon found at {path}")
            print(f"...color override is {color}" if color is not None else "...using default color")
            print(f"...backgroundColor override is {background_color}" if background_color is not None else "...using default backgroundColor")
            print(f"...borderColor override is {border_color}" if border_color is not None else "...using default borderColor")

        return True

[C++ Version]

Python Node Type Scheduling Hints

Specifying scheduling hints makes it easier for the OmniGraph scheduler to optimize the scheduling of node evaluation.

class OgnNodeSchedulingHints:
    @staticmethod
    def compute(db) -> bool:
        scheduling_hints = db.abi_node.get_node_type().get_scheduling_hints()

        # Ordinarily you would not need to access this scheduling hints as it is mainly for OmniGraph's use,
        # however it is available through the ABI so you can access it at runtime if you wish.
        print(f"Is this node threadsafe? {scheduling_hints.get_thread_safety()}")
        return True

[C++ Version]

Python Singleton Node Types

Specifying that a node type is a singleton creates a consistently named piece of metadata that can be checked to see if multiple instances of that node type will be allowed in a graph or its child graphs. Attempting to create more than one of such node types in the same graph or any of its child graphs will result in an error.

import omni.graph.core as og
class OgnNodeSingleton:
    @staticmethod
    def compute(db) -> bool:
        # The singleton value is just a special case of metadata. The hardcoded key is in the Python namespace
        singleton_value = db.get_metadata(og.MetadataKeys.SINGLETON)
        if singleton_value and singleton_value[0] == "1":
            print("I am a singleton")

        return True

[C++ Version]

Python Node Type Tags

Specifying the node tags creates a consistently named piece of metadata that the UI can use to present a more friendly grouping of the node types to the user.

import omni.graph.core as og
class OgnNodeTags:
    @staticmethod
    def compute(db) -> bool:
        # The tags value is just a special case of metadata. The hardcoded key is in the Python namespace
        print(f"My tags are {db.get_metadata(og.MetadataKeys.TAGS)}")

        return True

This example introduces a simple helper data structure og.MetadataKeys, which contains strings set to the key values of special internal metadata. These are metadata elements managed by the code generator. Using these names ensures consistent access.

og.MetadataKeys.ALLOWED_TOKENS       # On attributes of type token, a CSV formatted comma-separated list of potential legal values for the UI
og.MetadataKeys.DESCRIPTION          # On attributes and node types, contains their description from the .ogn file
og.MetadataKeys.EXTENSION            # On node types, contains the extension that owns this node type
og.MetadataKeys.HIDDEN               # On attributes and node types, indicating to the UI that they should not be shown
og.MetadataKeys.ICON_PATH            # On node types, contains the file path to the node's icon representation in the editor
og.MetadataKeys.ICON_BACKROUND_COLOR # On node types, overrides the background color of the node's icon
og.MetadataKeys.ICON_BORDER_COLOR    # On node types, overrides the border color of the node's icon
og.MetadataKeys.SINGLETON            # On node types its presence indicates that only one of the node type may be created in a graph
og.MetadataKeys.TAGS                 # On node types, a comma-separated list of tag categories for the type
og.MetadataKeys.UI_NAME              # On attributes and node types, user-friendly name specified in the .ogn file

[C++ Version]

Python Token Access

Python properties are used for convenience in accessing the predefined token values. As tokens are represented directly as strings in Python there is no need to support translation between strings and tokens as there is in C++.

class OgnNodeTokens:
    @staticmethod
    def compute(db) -> bool:

        print(f"The name for red is {db.tokens.red}")
        print(f"The name for green is {db.tokens.green}")
        print(f"The name for blue is {db.tokens.blue}")

        return True

[C++ Version]

Python Node Type UI Name Access

Specifying the node UI name creates a consistently named piece of metadata that the UI can use to present a more friendly name of the node type to the user.

import omni.graph.core as og
class OgnNodeUiName:
    @staticmethod
    def compute(db) -> bool:
        # The uiName value is just a special case of metadata. The hardcoded key is in the Python namespace
        print("Call me ", db.get_metadata(og.MetadataKeys.UI_NAME))

        return True

[C++ Version]

Simple Python Attribute Data Type

Accessors are created on the generated database class that return Python accessor objects that wrap the underlying attribute data, which lives in Fabric. As Python does not have the same flexibility with numeric data types there is some conversion performed. i.e. a Python number is always 64 bits so it must truncate when dealing with smaller attributes, such as int or uchar.

class OgnTokenStringLength:
    @staticmethod
    def compute(db) -> bool:

        # Access pattern is "db", the database, "inputs", the attribute's access type, and "token", the name the
        # attribute was given in the .ogn file
        #
        # Local variables can be used to clarify the intent, but are not necessary. As a matter of consistency we
        # use PEP8 conventions for local variables. Attribute names may not exactly follow the naming conventions
        # since they are mutually exclusive between C++ and Python (camelCase vs. snake_case)
        token_to_measure = db.inputs.token

        # Simple assignment to the output attribute's accessor is all you need to do to set the value, as it is
        # pointing directly to that data.
        db.outputs.length = len(token_to_measure)

        return True

[C++ Version]

Tuple Python Attribute Data Type

Tuples, arrays, and combinations of these all use the numpy array types as return values as opposed to a plain Python list such as List[float, float, float]. This plays a big part in efficiency as the numpy arrays can point directly to the Fabric data to minimize data copying.

Values are returned through the same kind of accessor as for simple data types, only differing in the returned data types.

class OgnVectorMultiply:
    @staticmethod
    def compute(db) -> bool:

        # The fact that everything is in numpy makes this kind of calculation trivial
        db.outputs.product = db.inputs.vector1.reshape(4,1) @ db.inputs.vector2.reshape(1,4)

        # Here db.inputs.vector1.shape = db.inputs.vector1.shape = (4,), db.outputs.product.shape = (4,4)
        return True

[C++ Version]

Role Python Attribute Data Type

Roles are stored in a parallel structure to the attributes as properties. For example db.inputs.color will have a corresponding property db.role.inputs.color. For convenience, the legal role names are provided as constants in the database class. The list of role names corresponds to the role values in the omni.graph.core.AttributeRole enum:

  • ROLE_COLOR

  • ROLE_EXECUTION

  • ROLE_FRAME

  • ROLE_NORMAL

  • ROLE_POINT

  • ROLE_QUATERNION

  • ROLE_TEXCOORD

  • ROLE_TIMECODE

  • ROLE_TRANSFORM

  • ROLE_VECTOR

class OgnPointsToVector:
    @staticmethod
    def compute(db) -> bool:

        # In Python the wrapper is only a property so the role has to be extracted from a parallel structure
        if db.role.inputs.point1 != db.ROLE_POINT:
            db.log_error(f"Cannot convert role {db.role.inputs.point1} to {db.ROLE_POINT}")
            return False
        if db.role.inputs.point2 != db.ROLE_POINT:
            db.log_error(f"Cannot convert role {db.role.inputs.point2} to {db.ROLE_POINT}")
            return False
        if db.role.outputs.vector != db.ROLE_POINT:
            db.log_error(f"Cannot convert role {db.role.inputs.vector} to {db.ROLE_VECTOR}")
            return False

        # The actual calculation is a trivial numpy call
        db.outputs.vector = db.inputs.point2 - db.inputs.point1

        return True

[C++ Version]

Array Python Attribute Data Type

As with tuple values, all array values in Python are represented as numpy.array types.

import numpy as np
class OgnPartialSums:
    @staticmethod
    def compute(db) -> bool:

        # This is a critical step, setting the size of the output array. Without this the array has no memory in
        # which to write.
        #
        # As the Python wrapper is a property, in order to get and set the size a secondary property is introduced
        # for array data types which have the same name as the attribute with "_size" appended to it. For outputs
        # this property also has a setter, which accomplishes the resizing.
        #
        db.outputs.partialSums_size = db.inputs.array_size

        # Always explicitly handle edge cases as it ensures your node doesn't disrupt evaluation
        if db.outputs.partialSums_size == 0:
            return True

        # IMPORTANT:
        #     The data value returned from accessing the property is a numpy array whose memory location was
        #     allocated by Fabric. As such you cannot use the numpy functions that resize the arrays as they will
        #     not use Fabric data.

        # However, since the array attribute data is wrapped in a numpy array you can use numpy functions that
        # modify data in place to make efficient use of memory.
        #
        db.inputs.array.cumsum(out=db.outputs.partialSums)

        # A second way you can assign array data is to collect the data externally and then do a simple list
        # assignment. This is less efficient as it does a physical copy of the entire list, though more flexible as
        # you can arbitrarily resize your data before assigning it. If you use this approach you skip the step of
        # setting the output array size as the assignment will do it for you.
        #
        #   # Using numpy
        #   output_list = np.cumsum(db.inputs.array)
        #   db.outputs.partialSums = output_list
        #
        #   # Using Python lists
        #   output_list = [value for value in db.inputs.array]
        #   for index, value in enumerate(output_list[:-1]):
        #       output_list[index + 1] = output_list[index + 1] + output_list[index]
        #   db.outputs.partialSum = output_list
        #
        #   # numpy is smart enough to do element-wise copy, but in this case you do have to preset the size
        #   output_list = np.cumsum(db.inputs.array)
        #   db.outputs.partialSums_size = db.inputs.array_size
        #   db.outputs.partialSums[:] = output_list[:]
        #

        return True

[C++ Version]

Tuple-Array Python Attribute Data Type

As with simple tuple values and array values the tuple-array values are also represented as numpy.array types. The numpy objects returned use the Fabric memory as their storage so they can be modified directly when computing outputs. As with regular arrays, you must first set the size required so that the right amount of memory can be allocated by Fabric.

class OgnCrossProducts:
    @staticmethod
    def compute(db) -> bool:

        # It usually keeps your code cleaner if you put your attribute wrappers into local variables, avoiding
        # the constant use of the "db.inputs" or "db.outputs" namespaces.
        a = db.inputs.a
        b = db.inputs.b
        crossProduct = db.outputs.crossProduct

        # This node chooses to make mismatched array lengths an error. You could also make it a warning, or just
        # simply calculate the result for the minimum number of available values.
        if db.inputs.a_size != db.inputs.b_size:
            db.log_error(f"Input array lengths do not match - '{db.inputs.a_size}' vs. '{db.inputs.b_size}'")
            return False

        # As with simple arrays, the size of the output tuple-array must be set first to allocate Fabric memory.
        db.outputs.crossProduct_size = db.inputs.a_size

        # Edge case is easily handled
        if db.inputs.a_size == 0:
            return False

        # The numpy cross product returns the result so there will be a single copy of the result onto the output.
        # numpy handles the iteration over the array so this one line does the entire calculation.
        crossProduct = np.cross(a, b)
        # This common syntax will do exactly the same thing
        #    crossProduct[:] = np.cross(a, b)[:]

        return True

[C++ Version]

String Python Attribute Data Type

String attributes are a bit unusual in Python. In Fabric they are implemented as arrays of characters but they are exposed in Python as plain old str types. The best approach is to manipulate local copies of the string and then assign it to the result when you are finished.

class OgnReverseString:
    @staticmethod
    def compute(db) -> bool:

        # In Python the wrapper to string attributes provides a standard Python string object.
        # As the wrapper is a property the assignment of a value uses the setter method to both allocate the
        # necessary space in Fabric and copy the values.

        db.outputs.result = db.inputs.original[::-1]

        return True

Important

Although strings are implemented in Fabric as arrays the fact that strings are immutable in Python means you don’t want to use the array method of resizing (i.e. setting the db.outputs.stringAttribute_size property). You can allocate it, but string elements cannot be assigned so there is no way to set the individual values.

[C++ Version]

Extended Python Attribute Data Type - Any

Extended attribute types have extra information that identifies the type they were resolved to at runtime. The access to this information is achieved by wrapping the attribute value in the same way as Bundle Python Attribute Data Type.

The Python property for the attribute returns an accessor rather than the value itself. This accessor has the properties “.value”, “.name”, and “.type” so that the type resolution information can be accessed directly. In addition, variations of the “.value” method specific to each memory space are provided as the properties “.cpu_value” and “.gpu_value”.

For example, the value for the input named a can be found at db.inputs.a.value, and its resolved type is at db.inputs.a.type.

class OgnMultiplyNumbers:
    @staticmethod
    def compute(db) -> bool:

        # Full details on handling extended types can be seen in the example for the "any" type. This example
        # shows only the necessary parts to handle the two types accepted for this union type (float and double).
        # The underlying code is all the same, the main difference is in the fact that the graph only allows
        # resolving to types explicitly mentioned in the union, rather than any type at all.

        # Use the exception system to implicitly check the resolved types. Unresolved types will not have accessible
        # data and raise an exception.
        try:
            db.outputs.product = np.mult(db.inputs.a, db.inputs.b)
        except Exception as error:
            db.log_error(f"Multiplication could not be performed: {error}")
            return False

        return True

[C++ Version]

The extended data types must all be resolved before calling into the compute method. The generated code handles that for you, executing the equivalent of these calls for extended inputs a and b, and extended output sum, preventing the call to compute() if any of the types are unresolved.

if db.inputs.a.type.base_type == og.BaseDataType.UNKNOWN:
    return False
if db.inputs.b.type.base_type == og.BaseDataType.UNKNOWN:
    return False
if db.outputs.sum.type.base_type == og.BaseDataType.UNKNOWN:
    return False

Extended Python Attribute Data Type - Union

The generated interface for union types is exactly the same as for any types. There is just a tacit agreement that the resolved types will always be one of the ones listed in the union type description.

class OgnMultiplyNumbers:
    @staticmethod
    def compute(db) -> bool:

        # Full details on handling extended types can be seen in the example for the "any" type. This example
        # shows only the necessary parts to handle the two types accepted for this union type (float and double).
        # The underlying code is all the same, the main difference is in the fact that the graph only allows
        # resolving to types explicitly mentioned in the union, rather than any type at all.

        # Use the exception system to implicitly check the resolved types. Unresolved types will not have accessible
        # data and raise an exception.
        try:
            db.outputs.product = np.mult(db.inputs.a, db.inputs.b)
        except Exception as error:
            db.log_error(f"Multiplication could not be performed: {error}")
            return False

        return True

[C++ Version]

Bundle Python Attribute Data Type

Bundle attribute information is accessed the same way as information for any other attribute type. As an aggregate, the bundle can be treated as a container for attributes, without any data itself.

class OgnMergeBundles:
    @staticmethod
    def compute(db) -> bool:

        bundleA = db.inputs.bundleA
        bundleB = db.inputs.bundleB
        mergedBundle = db.outputs.bundle

        # Bundle assignment means "assign all of the members of the RHS bundle to the LHS bundle". It doesn't
        # do a deep copy of the bundle members.
        mergedBundle = bundleA

        # Bundle insertion adds the contents of a bundle to an existing bundle. The bundles may not have members
        # with the same names
        mergedBundle.insert_bundle(bundleB)

        return True

[C++ Version]

When you want to get at the actual data, you use the bundle API to extract the runtime attribute accessors from the bundle for those attributes you wish to process.

import omni.graph.core as og
FLOAT_TYPE = og.Type(og.BaseDataType.FLOAT)

class OgnCalculateBrightness:
    def brightness_from_rgb(self, r: float, g: float, b: float) -> float:
        """The actual algorithm to run using a well-defined conversion"""
        return (r * (299.0) + (g * 587.0) + (b * 114.0)) / 256.0

    @staticmethod
    def compute(db) -> bool:
        # Retrieve the bundle accessor
        color = db.inputs.color

        # Using the bundle accessor, try to retrieve the RGB color members. In this case the types have to be
        # float, though in a more general purpose node you might also allow for double, half, and int types.
        r = color.attribute_by_name(db.tokens.r)
        g = color.attribute_by_name(db.tokens.g)
        b = color.attribute_by_name(db.tokens.b)
        # Validity of a member is a boolean
        if r.type == FLOAT_TYPE and g.type == FLOAT_TYPE and b.type == FLOAT_TYPE:
            db.outputs.brightness.value = OgnCalculateBrightness.brightness_from_rgb(r.value, g.value, b.value)
            return True

        # Having failed to extract RGB members, do the same check for CMYK members
        c = color.attribute_by_name(db.tokens.c)
        m = color.attribute_by_name(db.tokens.m)
        y = color.attribute_by_name(db.tokens.y)
        k = color.attribute_by_name(db.tokens.k)
        if c.type == FLOAT_TYPE and m.type == FLOAT_TYPE and y.type == FLOAT_TYPE and k.type == FLOAT_TYPE:
            db.outputs.brightness.value = OgnCalculateBrightness.brightness_from_rgb(
                (1.0 - c / 100.0) * (1.0 - k / 100.0),
                (1.0 - m / 100.0) * (1.0 - k / 100.0),
                (1.0 - y / 100.0) * (1.0 - k / 100.0)
            )
            return True

        # You could be more verbose about the reason for the problem as there are a few different scenarios:
        #    - some but not all of r,g,b or c,m,y,k were in the bundle
        #    - none of the color components were in the bundle
        #    - some or all of the color components were found but were of the wrong data type
        db.logError("Neither the groups (r, g, b) nor (c, m, y, k) are in the color bundle. Cannot compute brightness")
        return False

Tip

Although you access them in completely different ways the attributes that are bundle members use the same accessors as the extended attribute types. See further information in Extended Attribute Data Type - Any

This documentation for bundle access is pulled directly from the code. It removes the extra complication in the accessors required to provide proper typing information for bundle members and shows the appropriate calls in the bundle attribute API.

"""
# A bundle can be described as an opaque collection of attributes that travel together through the graph, whose
# contents and types can be introspected in order to determine how to deal with them. This section describes how
# the typical node will interface with the bundle content access. Use of the attributes within the bundles is the
# same as for the extended type attributes, described with their access methods.
#
# An important note regarding GPU bundles is that the bundle itself always lives on the CPU, specifying a memory
# space of "GPU/CUDA" for the bundle actually means that the default location of the attributes it contains will
# be on the GPU.
#
# The main bundle is extracted the same as any other attribute, by referencing its generated database location.
# For this example the bundle will be called "color" and it will have members that could either be the set
# ("r", "g", "b", "a") or the set ("c", "m", "y", "k") with the obvious implications of implied color space.

# As with other attribute types the bundle attribute functions are available through an accessor
color_bundle = db.inputs.color

# The accessor can determine if it points to valid data through a property
valid_color = color_bundle.valid

# If you want to call the underlying Bundle ABI directly you can access the og.Bundle object
bundle_object = color_bundle.bundle

# It can be queried for the number of attributes it holds
bundle_attribute_count = color_bundle.size

# It can have its contents iterated over, where each element in the iteration is an accessor of the bundled attribute
for (bundled_attribute in color_bundle.attributes)
    pass

# It can be queried for an attribute in it with a specific name
bundled_attribute = color_bundle.attribute_by_name(db.tokens.red)

# You can get naming information to identify where the bundle is stored you can also get a path
bundle_path = color_bundle.path

# *** The rest of these methods are for output bundles only, as they change the makeup of the bundle

# It can have its contents (i.e. attribute membership) cleared
computed_color_bundle.clear()

# It can be assigned to an output bundle, which merely transfers ownership of the bundle.
# The property setter for the bundle member is the mechanism for this.
color_bundle.bundle = some_other_bundle

# This is accomplished with the insert utility function, which can insert a number of different types of objects
# into a bundle. (The type of data it is passed determines what will be inserted.)
#
# The above function uses this variation, which inserts the bundle members into an existing bundle
computed_color_bundle.insert(color_bundle)

# It can have a single attribute from another bundle inserted into its current list, like if you don't want
# the transparency value in your output color
computed_color_bundle.clear()
computed_color_bundle.insert(color_bundle.attribute_by_name(db.tokens.red))
computed_color_bundle.insert(color_bundle.attribute_by_name(db.tokens.green))
computed_color_bundle.insert(color_bundle.attribute_by_name(db.tokens.blue))

# Optionally, the attribute can be renamed when adding to the bundle by passing the attribute and name as a 2-tuple
red_attribute = color_bundle.attribute_by_name(db.tokens.red)
computed_color_bundle.insert((red_attribute, db.tokens.scarlett))

# It can also add a brand new attribute with a specific type and name as a 2-tuple
og.Type FLOAT_TYPE(og.BaseDataType.FLOAT)
computed_color_bundle.insert((FLOAT_TYPE, db.tokens.opacity)

# *** When attributes are extracted from a bundle they will also be enclosed in a wrapper class og.RuntimeAttribute

# The wrapper class has access to the attribute description information, specifically the name and type
red_name = red_attribute.name
red_type = red_attribute.type

# Array attributes have a "size" property, which can also be set on output or state attributes
point_array = db.inputs.mesh.attribute_by_name(db.tokens.points)
deformed_point_array = db.outputs.mesh.attribute_by_name(db.tokens.points)
deformed_point_array.size = point_array.size

# Default value access is done through the value property, which is writable on output or state attributes
red_input = db.inputs.color.attribute_by_name(db.tokens.red)
red_output = db.outputs.color.attribute_by_name(db.tokens.red)
red_output.value = 1.0 - red_input.value

# By default the above functions operate in the same memory space as was defined by the bundled that contained the
# attribute. If you wish to be more explicit about where the memory lives you can access the specific versions of
# value properties that force either CPU or GPU memory space
if on_gpu:
    call_cuda_code(red_output.gpu_value, red_input.gpu_value)
else:
    red_output.cpu_value = 1.0 - red_input.cpu_value

# Lastly, on the rare occasion you need direct access to the attribute's ABI through the underlying type
# og.AttributeData you can access it through the abi property
my_attribute_data = red_attribute.abi
"""

[C++ Version]

Python Attribute Memory Location

class OgnMemoryType:
    @staticmethod
    def compute(db) -> bool:

        # The operation specifies moving the points data onto the GPU for further computation if the size of
        # the input data reaches a threshold where that will make the computation more efficient.
        # (This particular node just moves data; in practice you would perform an expensive calculation on it.)
        if db.inputs.points.size > db.inputs.sizeThreshold:
            # The gpu property forces the data onto the GPU. It may or may not perform CPU->GPU copies under the
            # covers. Fabric handles all of those details so that you don't have to.
            db.outputs.points.gpu = db.inputs.points.gpu
        else:
            # The cpu property forces the data onto the CPU. It may or may not perform GPU->CPU copies under the
            # covers. Fabric handles all of those details so that you don't have to.
            db.outputs.points.cpu = db.inputs.points.cpu
        return True

[C++ Version]

Python Attribute CPU Pointers to GPU Data

Note

Although this value takes effect at the attribute level the keyword is only valid at the node level. All attributes in a node will use the same type of CUDA array pointer referencing.

class OgnCudaPointers:
    @staticmethod
    def compute(db) -> bool:

        # When the *cudaPointers* keyword is set to *cpu* this wrapped array will contain a CPU pointer that
        # references the GPU array data. If not, it would have contained a GPU pointer that references the GPU
        # array data and not been able to be dereferenced on the CPU side.
        callCudaFunction(db.inputs.cudaPoints, db.outputs.cudaPoints)
        return True

[C++ Version]

Python Attribute Metadata Access

When attributes have metadata added to them they can be accessed through the ABI attribute interface.

class OgnStarWarsCharacters:
    @staticmethod
    def compute(db) -> bool:
        anakin_attr = db.attributes.inputs.anakin
        # Specifically defined metadata can be accessed by name
        print(f"Anakin's secret is {db.get_metadata('secret', anakin_attr)}")

        # Some metadata is automatically added; you can see it by iterating over all of the existing metadata.
        for metadata_name, metadata_value in anakin_attr.get_all_metadata():
            print(f"Metadata for {metadata_name} is {metadata_value}")

        # You can also access it directly from the database's metadata interface, either from the node type...
        print(f"Node UI Name is {db.get_metadata(og.MetadataKeys.UI_NAME)}")
        # ...or from a specific attribute
        print(f"Attribute UI Name is {db.get_metadata(og.MetadataKeys.UI_NAME, anakin_attr)}")

        return True

[C++ Version]

Optional Python Attributes

Since Python values are extracted through the C++ ABI bindings they don’t have a direct validity check so the validity of optional attributes must be checked indirectly. If a Python attribute value returns the special None value then the attribute is not valid. It may also raise a TypeError or ValueError exception, indicating there was a mismatch between the data available and the type expected.

import random
from contextlib import suppress
class OgnShoes:
    SHOE_TYPES=["Runners", "Slippers", "Oxfords"]

    @staticmethod
    def compute(db) -> bool:

        shoe_index = random.randint(0, 2)
        shoe_type_name = OgnShoes.SHOE_TYPES[shoe_index]

        # If the shoe is a type that has laces then append the lace type name
        if shoe_index != 1:
            # As this is an optional value it may or may not be valid at this point.
            # This check happens automatically with required attributes. With optional ones it has to be done when used.
            if db.attributes.inputs.shoeLaceStyle.isValid():
                # The attribute may be valid but the data retrieval may still fail. In Python this is flagged in one of
                # two ways - raising an exception, or returning None. Both indicate the possibility of invalid data.
                # In this node we've chosen to silently ignore expected but invalid shoelace style values. We could
                # equally have logged an error or a warning.
                with suppress(ValueError, TypeError):
                    shoelace_style = db.inputs.shoelaceStyle
                    if shoelace_style is not None:
                        shoe_type_name += f" with {shoelace_style} laces"

        db.outputs.shoeType = shoe_type_name
        return True

[C++ Version]

Python Attribute UI Name Access

Specifying the attribute uiName creates a consistently named piece of metadata that the UI can use to present a more friendly version of the attribute name to the user. It can be accessed through the regular metadata ABI, with some constants provided for easier access.

import omni.graph.core as og
class OgnAttributeUiName:
    @staticmethod
    def compute(db) -> bool:
        # The uiName value is just a special case of metadata
        print(f"Call me {db.get_metadata(og.MetadataKeys.UI_NAME, db.attributes.inputs.x)}")

        return True

[C++ Version]

Unvalidated Python Attributes

For most attributes the generated code will check to see if the attribute is valid before it calls the compute() function. unvalidated attributes will not have this check made. If you end up using their value then you must make the call to the is_valid() method yourself first and react appropriately if invalid values are found. Further, for attributes with extended types you must verify that they have successfully resolved to a legal type.

import omni.graph.core as og
class OgnABTest:
    @staticmethod
    def compute(db) -> bool:
        choice = db.outputs.choice
        out_type = choice.type
        # Check to see which input is selected and verify that its data type matches the output resolved type
        if db.inputs.selectA:
            input_a = db.inputs.a
            if not input_a.is_valid() or input_a.type != out_type:
                db.log_error(f"Mismatched types at input a - '{input_a.type.get_ogn_type_name()}' versus '{out_type.get_ogn_type_name()}'")
                return False
            choice.value = input_a.value
        else:
            input_b = db.inputs.b
            if not input_b.is_valid() or input_b.type != out_type:
                db.log_error(f"Mismatched types at input b - '{input_b.type.get_ogn_type_name()}' versus '{out_type.get_ogn_type_name()}'")
                return False
            choice.value = input_b.value
        return True

[C++ Version]

Dynamic Python Attributes

In addition to attributes statically defined through a .ogn file, you can also dynamically add attributes to a single node by using the ABI call og.Node.create_attribute(...). When you do so, the Python database interface will automatically pick up these new attributes and provide access to their data in exactly the same way as it does for regular attributes. (i.e. db.inputs.X for the value, db.attributes.input.X for the underlying og.Attribute, db.roles.inputs.X for the attribute role, etc.)

The way you test for such an attribute’s existence inside a compute() method is to capture the AttributeError exception.

from contextlib import suppress
class OgnDynamicDuo:
    @staticmethod
    def compute(db) -> bool:
        try:
            # Testing for the existence of the dynamic input boolean attribute "Robin"
            db.outputs.batman = "Duo" if db.inputs.robin
        except AttributeError:
            db.outputs.batman = "Solo"
        return True

Note

There is no C++ equivalent to this feature. Dynamic attributes will be available on the Python accessors to the C++ node but the C++ code can only access the attribute data by using the low level ABI.

Python Nodes With Internal State

Unlike C++ classes it is not as easy to determine if a Python class contains data members that should be interpreted as state information. Instead, the Python node class will look for a method called internal_state(), which should return an object containing state information to be attached to a node. Once the internal state has been constructed it is not modified by OmniGraph until the node is released, it is entirely up to the node how and when to modify the data.

That information will be in turn made accessible through the database class using the property db.internal_state.

class OgnStateNode:
    class State:
        """Container object holding the node's state information"""
        def __init__(self):
            self.counter = 0

    @staticmethod
    def internal_state():
        """Returns an object that will contain per-node state information"""
        return OgnStateNode.State()

    @staticmethod
    def compute(db) -> bool:
        print(f"This node has been evaluated {db.internal_state.counter} times")
        db.internal_state.counter += 1

        return True

[C++ Version]

Python Nodes With Version Upgrades

To provide code to upgrade a node from a previous version to the current version you must override the ABI function update_node_version(). The current context and node to be upgraded are passed in, as well as the old version at which the node was created and the new version to which it should be upgraded. Passing both values allows you to upgrade nodes at multiple versions in the same code.

This example shows how a new attribute is added using the og.Node ABI interface.

import carb
import omni.graph.core as og
class OgnMultiply:
    @staticmethod
    def compute(db) -> bool:
        db.outputs.result = db.inputs.a * db.inputs.b + db.inputs.offset

        return True

    @staticmethod
    def update_node_version(context, node, old_version, new_version):
        if old_version == 1 and new_version == 3:
            node.create_attribute("inputs:offset", og.Type(og.BaseDataType.FLOAT))
            return True
        else:
            # Always good practice to flag unknown version changes so that they are not forgotten
            carb.log_error(f"Do not know how to upgrade Multiply from version {old_version} to {new_version}")
            return False

[C++ Version]