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.
For examples specific to Action Graph see Action Graph Code Samples - Python.
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
Contents
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
# Class method to get the internal state data shared by all instances of an authored node
database.shared_internal_state(node)
# Class method to get the internal state data attached to a specific instance of an authored node
database.per_instance_internal_state(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.
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
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.
import omni.graph.tools.ogn as ogn
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(ogn.MetadataKeys.ICON_PATH)
color = db.get_metadata(ogn.MetadataKeys.ICON_COLOR)
background_color = db.get_metadata(ogn.MetadataKeys.ICON_BACKGROUND_COLOR)
border_color = db.get_metadata(ogn.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
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
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.tools.ogn as ogn
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(ogn.MetadataKeys.SINGLETON)
if singleton_value and singleton_value[0] == "1":
print("I am a singleton")
return True
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
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(ogn.MetadataKeys.UI_NAME))
return True
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
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
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:
import omni.graph.core as og
@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 != og.AttributeRole.POSITION:
db.log_error(f"Cannot convert role {db.role.inputs.point1} to {og.AttributeRole.POSITION}")
return False
if db.role.inputs.point2 != og.AttributeRole.POSITION:
db.log_error(f"Cannot convert role {db.role.inputs.point2} to {og.AttributeRole.POSITION}")
return False
if db.role.outputs.vector != og.AttributeRole.POSITION:
db.log_error(f"Cannot convert role {db.role.inputs.vector} to {og.AttributeRole.VECTOR}")
return False
# The actual calculation is a trivial numpy call
db.outputs.vector = db.inputs.point2 - db.inputs.point1
return True
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
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
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.
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
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
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
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
"""
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
Node Type Categories
Categories are added as metadata to the node and can be accessed through the standard metadata interface.
import omni.graph.tools.ogn as ogn
class OgnNodeCategories:
@staticmethod
def compute(db) -> bool:
# The categories value is just a special case of metadata. The hardcoded key is in the Python namespace
categories_value = db.get_metadata(ogn.MetadataKeys.CATEGORIES)
if categories_value:
print(f"These are my categories {categories_value.split(',')}")
return True
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
Python Attribute Metadata Access
When attributes have metadata added to them they can be accessed through the ABI attribute interface.
import omni.graph.tools.ogn as ogn
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(ogn.MetadataKeys.UI_NAME)}")
# ...or from a specific attribute
print(f"Attribute UI Name is {db.get_metadata(ogn.MetadataKeys.UI_NAME, anakin_attr)}")
return True
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
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.tools.ogn as ogn
class OgnAttributeUiName:
@staticmethod
def compute(db) -> bool:
# The uiName value is just a special case of metadata
print(f"Call me {db.get_metadata(ogn.MetadataKeys.UI_NAME, db.attributes.inputs.x)}")
return True
Python Attribute UI Type Access
Specifying the attribute uiType tells the property panel that this attribute should be shown with custom widgets. - For path, string, and token attributes, a ui type of “filePath” will show file browser widgets - For 3- and 4-component numeric tuples, a ui type of “color” will show the color picker widget
import omni.graph.tools.ogn as ogn
class OgnAttributeUiType:
@staticmethod
def compute(db) -> bool:
# The uiType value is just a special case of metadata
print(f"The property panel ui type is {db.get_metadata(ogn.MetadataKeys.UI_TYPE, '(default)')}")
return True
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
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.
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 else "Unknown"
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
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
# 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