Basic Concepts¶
Node basics¶
The Node class is the primary object in conduit.
Think of it as a hierarchical variant object.
import conduit
n = conduit.Node()
n["my"] = "data"
print(n)
my: "data"
The Node class supports hierarchical construction.
n = conduit.Node()
n["my"] = "data";
n["a/b/c"] = "d";
n["a"]["b"]["e"] = 64.0;
print(n)
print("total bytes: {}\n".format(n.total_strided_bytes()))
my: "data"
a:
b:
c: "d"
e: 64.0
total bytes: 15
Borrowing from JSON (and other similar notations), collections of named nodes are called Objects and collections of unnamed nodes are called Lists, all other types are leaves that represent concrete data.
n = conduit.Node()
n["object_example/val1"] = "data"
n["object_example/val2"] = 10
n["object_example/val3"] = 3.1415
for i in range(5):
l_entry = n["list_example"].append()
l_entry.set(i)
print(n)
object_example:
val1: "data"
val2: 10
val3: 3.1415
list_example:
- 0
- 1
- 2
- 3
- 4
You can iterate through a Node’s children.
n = conduit.Node()
n["object_example/val1"] = "data"
n["object_example/val2"] = 10
n["object_example/val3"] = 3.1415
for i in range(5):
l_entry = n["list_example"].append()
l_entry.set(i)
print(n)
for v in n["object_example"].children():
print("{}: {}".format(v.name(),str(v.node())))
print()
for v in n["list_example"].children():
print(v.node())
object_example:
val1: "data"
val2: 10
val3: 3.1415
list_example:
- 0
- 1
- 2
- 3
- 4
val1: "data"
val2: 10
val3: 3.1415
0
1
2
3
4
Behind the scenes, Node instances manage a collection of memory spaces.
n = conduit.Node()
n["my"] = "data"
n["a/b/c"] = "d"
n["a"]["b"]["e"] = 64.0
print(n.info())
mem_spaces:
0x7fc669907820:
path: "my"
type: "allocated"
bytes: 5
allocator_id: 0
0x7fc66995a190:
path: "a/b/c"
type: "allocated"
bytes: 2
allocator_id: 0
0x7fc669941920:
path: "a/b/e"
type: "allocated"
bytes: 8
allocator_id: 0
total_bytes_allocated: 15
total_bytes_mmaped: 0
total_bytes_compact: 15
total_strided_bytes: 15
There is no absolute path construct, all paths are fetched relative to the current node (a leading
/
is ignored when fetching). Empty paths names are also ignored, fetching a///b
is
equalvalent to fetching a/b
.
Bitwidth Style Types¶
When sharing data in scientific codes, knowing the precision of the underlining types is very important.
Conduit uses well defined bitwidth style types (inspired by NumPy) for leaf values. In Python, leaves are provided as NumPy ndarrays.
n = conduit.Node()
n["test"] = numpy.uint32(100)
print(n)
test: 100
Standard Python numeric types will be mapped to bitwidth style types.
n = conduit.Node()
n["test"] = 10
print(n.schema())
{
"test": {"dtype":"int64","number_of_elements": 1,"offset": 0,"stride": 8,"element_bytes": 8,"endianness": "little"}
}
- Supported Bitwidth Style Types:
- signed integers: int8,int16,int32,int64
- unsigned integers: uint8,uint16,uint32,uint64
- floating point numbers: float32,float64
- Conduit provides these types by constructing a mapping for the current platform the from the following C++ types:
- char, short, int, long, long long, float, double, long double
When a set method is called on a leaf Node, if the data passed to the set is compatible with the Node’s Schema the data is simply copied.
Compatible Schemas¶
When passed a compatible Node, Node methods update
and update_compatible
allow you to copy data into Node or extend a Node with new data without
changing existing allocations.
Schemas do not need to be identical to be compatible.
You can check if a Schema is compatible with another Schema using the Schema::compatible(Schema &test) method. Here is the criteria for checking if two Schemas are compatible:
- If the calling Schema describes an Object : The passed test Schema must describe an Object and the test Schema’s children must be compatible with the calling Schema’s children that have the same name.
- If the calling Schema describes a List: The passed test Schema must describe a List, the calling Schema must have at least as many children as the test Schema, and when compared in list order each of the test Schema’s children must be compatible with the calling Schema’s children.
- If the calling Schema describes a leaf data type: The calling Schema’s and test Schema’s
dtype().id()
anddtype().element_bytes()
must match, and the calling Schemadtype().number_of_elements()
must be greater than or equal than the test Schema’s.
Here is a Python pseudocode example that shows the most common use of Node.compatible()
:
a = conduit.Node()
b = conduit.Node()
# In this example:
# the calling schema is `a.schema()`
# the test schema is `b.schema()`
# ask if `a` can already hold data described by `b`
if a.compatible(b) :
# data from `b` can be written to `a` without a new allocation
# ...
Differences between C++ and Python APIs¶
In Python, Node objects are reference-counted containers that hold C++ Node pointers. This provides a Python API similar to using references in C++. However, you should be aware of some key differences.
This provides similar API in Python to using references in C++, however there are a few key differences to be aware of.
The [] operator is different in that it will return not only Nodes, but numpy arrays depending on the context:
# setup a node with a leaf array
n = conduit.Node()
data = numpy.zeros((5,),dtype=numpy.float64)
n["my/path/to/data"] = data
# this will be an ndarray
my_data = n["my/path/to/data"]
print("== this will be an ndarray == ")
print("data: ", my_data)
print("repr: ",repr(my_data))
print()
# this will be a node
n_my_path = n["my/path"]
print("== this will be a node == ")
print("{node}\n", n_my_path)
print("{schema}\n",n_my_path.schema().to_yaml())
== this will be an ndarray ==
data: [0. 0. 0. 0. 0.]
repr: array([0., 0., 0., 0., 0.])
== this will be a node ==
{node}
to:
data: [0.0, 0.0, 0.0, 0.0, 0.0]
{schema}
to:
data:
dtype: "float64"
number_of_elements: 5
offset: 0
stride: 8
element_bytes: 8
endianness: "little"
If you are expecting a Node, the best way to access a subpath is using
fetch()
or fetch_existing()
:
# setup a node with a leaf array
n = conduit.Node()
data = numpy.zeros((5,),dtype=numpy.float64)
n["my/path/to/data"] = data
# this will be an ndarray
my_data = n["my/path/to/data"]
print("== this will be an ndarray == ")
print("data: ", my_data)
print("repr: ",repr(my_data))
print()
# equiv access via fetch (or fetch_existing)
# first fetch the node and then the array
my_data = n.fetch("my/path/to/data").value()
print("== this will be an ndarray == ")
print("data: ",my_data)
print("repr: ",repr(my_data))
print()
== this will be an ndarray ==
data: [0. 0. 0. 0. 0.]
repr: array([0., 0., 0., 0., 0.])
== this will be an ndarray ==
data: [0. 0. 0. 0. 0.]
repr: array([0., 0., 0., 0., 0.])
We use the const construct in C++ to provide additional seat belts for read-only
style access to Nodes. We don’t provide a similar overall construct in Python, but
the standard methods like fetch_existing()
do support these types
of use cases:
# setup a node with a leaf array
n = conduit.Node()
data = numpy.zeros((5,),dtype=numpy.float64)
n["my/path/to/data"] = data
# access via fetch existing
# first fetch the node
n_data = n.fetch_existing("my/path/to/data")
# then the value
my_data = n_data.value()
print("== this will be an ndarray == ")
print("data: ",my_data)
print("repr: ",repr(my_data))
print()
# using fetch_existing,
# if the path doesn't exist - we will get an Exception
try:
n_data = n.fetch_existing("my/path/TYPO/data")
except Exception as e:
print("Here is what went wrong:")
print(e)
== this will be an ndarray ==
data: [0. 0. 0. 0. 0.]
repr: array([0., 0., 0., 0., 0.])
Here is what went wrong:
file: /Users/harrison37/Work/github/llnl/conduit/src/libs/conduit/conduit_node.cpp
line: 14182
message:
Cannot fetch non-existent child "TYPO" from Node(my/path)