Tutorial 26 - Generic Math Node

This tutorial demonstrates how to compose nodes that perform mathematical operations in python using numpy. Using numpy has the advantage that it is api-compatible to cuNumeric. As demonstrated in the Extended Attributes tutorial, generic math nodes use extended attributes to allow inputs and outputs of arbitrary numeric types, specified using the “numerics” keyword.

"inputs": {
    "myNumbericAttribute": {
        "description": "Accepts an incoming connection from any type of numeric value",
        "type": ["numerics"]
    }
}

OgnTutorialGenericMathNode.ogn

The ogn file shows the implementation of a node named “omni.graph.tutorials.GenericMathNode”, which takes inputs of any numeric types and performs a multiplication.

 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
{
    "GenericMathNode": {
        "description": [
            "This is a tutorial node. It is functionally equivalent to the built-in Multiply node,",
            "but written in python as a practical demonstration of using extended attributes to ",
            "write math nodes that work with any numeric types, including arrays and tuples."
        ],
        "version": 1,
        "language": "python",
        "uiName": "Tutorial Python Node: Generic Math Node",
        "categories": "tutorials",
        "inputs": {
            "a": {
                "type": ["numerics"],
                "description": "First number to multiply",
                "uiName": "A"
            },
            "b": {
                "type": ["numerics"],
                "description": "Second number to multiply",
                "uiName": "B"
            }
        },
        "outputs": {
            "product": {
                "type": ["numerics"],
                "description": "Product of the two numbers",
                "uiName": "Product"
            }
        },
        "tests" : [
            {  "inputs:a": {"type": "int", "value": 2}, "inputs:b": {"type": "int", "value": 3}, "outputs:product": {"type": "int", "value": 6} },
            {  "inputs:a": {"type": "int", "value": 2}, "inputs:b": {"type": "int64", "value": 3}, "outputs:product": {"type": "int64", "value": 6} },
            {  "inputs:a": {"type": "int", "value": 2}, "inputs:b": {"type": "half", "value": 3}, "outputs:product": {"type": "float", "value": 6} },
            {  "inputs:a": {"type": "int", "value": 2}, "inputs:b": {"type": "float", "value": 3}, "outputs:product": {"type": "float", "value": 6} },
            {  "inputs:a": {"type": "int", "value": 2}, "inputs:b": {"type": "double", "value": 3}, "outputs:product": {"type": "double", "value": 6} },
            {  "inputs:a": {"type": "int64", "value": 2}, "inputs:b": {"type": "int64", "value": 3}, "outputs:product": {"type": "int64", "value": 6} },
            {  "inputs:a": {"type": "int64", "value": 2}, "inputs:b": {"type": "half", "value": 3}, "outputs:product": {"type": "double", "value": 6} },
            {  "inputs:a": {"type": "int64", "value": 2}, "inputs:b": {"type": "float", "value": 3}, "outputs:product": {"type": "double", "value": 6} },
            {  "inputs:a": {"type": "int64", "value": 2}, "inputs:b": {"type": "double", "value": 3}, "outputs:product": {"type": "double", "value": 6} },
            {  "inputs:a": {"type": "half", "value": 2}, "inputs:b": {"type": "half", "value": 3}, "outputs:product": {"type": "half", "value": 6} },
            {  "inputs:a": {"type": "half", "value": 2}, "inputs:b": {"type": "float", "value": 3}, "outputs:product": {"type": "float", "value": 6} },
            {  "inputs:a": {"type": "half", "value": 2}, "inputs:b": {"type": "double", "value": 3}, "outputs:product": {"type": "double", "value": 6} },
            {  "inputs:a": {"type": "float", "value": 2}, "inputs:b": {"type": "float", "value": 3}, "outputs:product": {"type": "float", "value": 6} },
            {  "inputs:a": {"type": "float", "value": 2}, "inputs:b": {"type": "double", "value": 3}, "outputs:product": {"type": "double", "value": 6} },
            {  "inputs:a": {"type": "double", "value": 2}, "inputs:b": {"type": "double", "value": 3}, "outputs:product": {"type": "double", "value": 6} },
            {
                "inputs:a": {"type": "double[2]", "value": [1.0, 42.0]}, "inputs:b": {"type": "double[2]", "value": [2.0, 1.0]},
                "outputs:product": {"type": "double[2]", "value": [2.0, 42.0]}
            },
            {
                "inputs:a": {"type": "double[]", "value": [1.0, 42.0]}, "inputs:b": {"type": "double", "value": 2.0},
                "outputs:product": {"type": "double[]", "value": [2.0, 84.0]}
            },
            {
                "inputs:a": {"type": "double[2][]", "value": [[10, 5], [1, 1]]}, 
                "inputs:b": {"type": "double[2]", "value": [5, 5]},
                "outputs:product": {"type": "double[2][]", "value": [[50, 25], [5, 5]]}
            }
        ]
    }
}

OgnTutorialGenericMathNode.py

The py file contains the implementation of the node. It takes two numeric inputs and performs a multiplication, demonstrating how to handle cases where the inputs are both numeric types but vary in precision, format or dimension.

 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
import omni.graph.core as og
import numpy as np

# Mappings of possible numpy dtypes from the result data type and back
dtype_from_basetype = {
    og.BaseDataType.INT: np.int32,
    og.BaseDataType.INT64: np.int64,
    og.BaseDataType.HALF: np.float16,
    og.BaseDataType.FLOAT: np.float32,
    og.BaseDataType.DOUBLE: np.float64,
}

supported_basetypes = [
    og.BaseDataType.INT,
    og.BaseDataType.INT64,
    og.BaseDataType.HALF,
    og.BaseDataType.FLOAT,
    og.BaseDataType.DOUBLE
]

basetype_resolution_table = [
    [0, 1, 3, 3, 4],  # Int
    [1, 1, 4, 4, 4],  # Int64
    [3, 4, 2, 3, 4],  # Half
    [3, 4, 3, 3, 4],  # Float
    [4, 4, 4, 4, 4],  # Double
]

class OgnTutorialGenericMathNode:
    """Node to multiple two values of any type"""
    @staticmethod
    def compute(db) -> bool:
        """Compute the product of two values, if the types are all resolved.

        When the types are not compatible for multiplication, or the result type is not compatible with the
        resolved output type, the method will log an error and fail
        """
        try:
            # To support multiplying array of vectors by array of scalars we need to broadcast the scalars to match the
            # shape of the vector array, and we will convert the result to whatever the result is resolved to
            atype = db.inputs.a.type
            btype = db.inputs.b.type
            rtype = db.outputs.product.type

            result_dtype = dtype_from_basetype.get(rtype.base_type, None)

            # Use numpy to perform the multiplication in order to automatically handle both scalar and array types
            # and automatically convert to the resolved output type
            if atype.array_depth > 0 and btype.array_depth > 0 and btype.tuple_count < atype.tuple_count:
                r = np.multiply(db.inputs.a.value, db.inputs.b.value[:, np.newaxis], dtype=result_dtype)
            else:
                r = np.multiply(db.inputs.a.value, db.inputs.b.value, dtype=result_dtype)

            db.outputs.product.value = r
        except Exception as error:          
            db.log_error(f"Multiplication could not be performed: {error}")
            return False

        return True

    @staticmethod
    def on_connection_type_resolve(node) -> None:
        # Resolves the type of the output based on the types of inputs
        atype = node.get_attribute("inputs:a").get_resolved_type()
        btype = node.get_attribute("inputs:b").get_resolved_type()
        productattr = node.get_attribute("outputs:product")
        producttype = productattr.get_resolved_type()

        # The output types can be only inferred when both inputs types are resolved.
        if (atype.base_type != og.BaseDataType.UNKNOWN and btype.base_type != og.BaseDataType.UNKNOWN
                and producttype.base_type == og.BaseDataType.UNKNOWN):  

            # Resolve the base type using the lookup table
            base_type = og.BaseDataType.DOUBLE

            a_index = supported_basetypes.index(atype.base_type)
            b_index = supported_basetypes.index(btype.base_type)

            if a_index >= 0 and b_index >= 0:
                base_type = supported_basetypes[basetype_resolution_table[a_index][b_index]]

            productattr.set_resolved_type(og.Type(base_type, max(atype.tuple_count, btype.tuple_count),
                max(atype.array_depth, btype.array_depth)))