Array Tree
A generic hierarchical tree data structure backed by NumPy structured arrays, with typed node payloads.
ArrayTree stores tree topology (parent, child, and sibling pointers) in a compact NumPy structured array for cache-friendly memory layout and efficient bulk operations. Payloads are stored separately in a typed list, so the tree works with any Python object — strings, dicts, dataclasses, or the included DataNode container for data + metadata.
Designed for CAE (Computer-Aided Engineering) workflows where trees represent assembly structures, material hierarchies, and analysis results with thousands of nodes carrying large numerical arrays.
Key features
- Generic —
ArrayTree[T]accepts any payload type - NumPy-backed topology — structured array with uint32 IDs, ~29 bytes per node
- Soft deletion — O(1) delete, explicit
repack()for compaction - Lazy loading —
LazyDataNodeloads array data on demand, releases to free memory - Frozen mode — prevent mutations after construction
- Zero hard dependencies beyond NumPy
Installation
pip install vcti-array-tree>=1.4.0
In pyproject.toml dependencies
dependencies = [
"vcti-array-tree>=1.4.0",
]
Quick Start
from vcti.arraytree import ArrayTree, DataNode
# Create a tree with any payload type
tree: ArrayTree[str] = ArrayTree()
# Add nodes (root is always id=0)
models = tree.add_node(0, data="models")
results = tree.add_node(0, data="results")
stress = tree.add_node(results, data="stress")
# Navigate
children = tree.get_children_ids(0) # [1, 2]
parent = tree.get_parent_id(stress) # 2 (results)
all_nodes = tree.iter_nodes(0) # [1, 2, 3] depth-first
# Access and update payloads
tree.get_node(stress) # "stress"
tree.set_node(stress, "von_mises_stress") # update payload
# Ancestry and depth
tree.get_ancestor_ids(stress) # [2, 0] (results, root)
tree.get_depth(stress) # 2
# Soft-delete and repack
tree.delete_node(models)
mapping = tree.repack() # compacts, remaps IDs
# Freeze for read-only access
tree.freeze()
With DataNode payloads
import numpy as np
from vcti.arraytree import ArrayTree, DataNode
tree: ArrayTree[DataNode] = ArrayTree()
tree.add_node(0, DataNode(
data=np.array([1.0, 2.0, 3.0]),
attributes={'units': 'mm', 'name': 'displacement'},
))
tree.add_node(0, DataNode(
attributes={'type': 'group', 'solver': 'NASTRAN'},
))
Error handling
from vcti.arraytree import ArrayTree
tree: ArrayTree[str] = ArrayTree()
node = tree.add_node(0, "data")
# KeyError — node doesn't exist
try:
tree.get_node(999)
except KeyError:
pass
# ValueError — node is deleted
tree.delete_node(node)
try:
tree.get_node(node)
except ValueError:
pass
# RuntimeError — tree is frozen
tree.freeze()
try:
tree.add_node(0, "blocked")
except RuntimeError:
pass
Core API
ArrayTree[T]
| Method | Description |
|---|---|
add_node(parent_id, data) |
Add child node, returns new ID |
set_node(node_id, data) |
Update payload for existing node |
delete_node(node_id, recursive) |
Soft-delete (marks inactive) |
get_node(node_id) |
Get payload (O(1)) |
get_parent_id(node_id) |
Get parent ID |
get_children_ids(node_id) |
List child IDs |
get_ancestor_ids(node_id) |
List ancestor IDs (parent to root) |
get_depth(node_id) |
Depth of node (root = 0) |
iter_nodes(root_id, recursive) |
Depth-first traversal |
get_all_node_ids(active_only) |
All node IDs |
repack(new_capacity) |
Remove deleted nodes, compact arrays |
freeze(payloads=False) |
Freeze structure; optionally freeze payloads too |
is_payloads_frozen() |
True if payloads are also frozen |
count_nodes(active_only) |
Count nodes |
get_capacity() |
Current pre-allocated capacity |
len(tree) |
Number of active nodes |
node_id in tree |
Check if node is active |
for nid in tree |
Iterate active node IDs |
get_path(node_id) |
Path from root to node |
tree.topology |
Read-only numpy view of tree structure |
tree.payloads |
Read-only tuple of payloads |
DataNode
Simple container for tree node payloads:
| Attribute | Type | Description |
|---|---|---|
data |
Any (ndarray, MaskedArray, etc.) or None | Data payload |
attributes |
dict[str, Any] | Metadata dictionary |
LazyDataNode
DataNode subclass with on-demand loading for large arrays:
from vcti.arraytree import ArrayTree, LazyDataNode
tree: ArrayTree[LazyDataNode] = ArrayTree()
node = LazyDataNode(
loader=lambda: load_stress_data(path, offset),
attributes={'units': 'MPa', 'name': 'stress'},
)
nid = tree.add_node(0, node)
# Attributes always available
tree.get_node(nid).attributes # {'units': 'MPa', 'name': 'stress'}
# Array loaded on demand
tree.get_node(nid).is_loaded # False
tree.get_node(nid).load() # calls loader, caches result
tree.get_node(nid).is_loaded # True
tree.get_node(nid).release() # frees array, can reload later
| Method/Property | Description |
|---|---|
load() |
Load array via callback (idempotent) |
release() |
Free array data, attributes stay resident |
is_loaded |
True if array is in memory |
Configuration
The ID dtype and sentinel value are configurable per tree:
import numpy as np
from vcti.arraytree import ArrayTree
tree = ArrayTree(id_dtype=np.uint64) # 64-bit node IDs
tree.id_dtype # dtype('uint64')
tree.invalid_id # 18446744073709551615
tree.tree_dtype # structured dtype for topology
The make_tree_dtype(id_dtype) helper builds the structured dtype for
external consumers (e.g., file loaders building topology arrays).
Exceptions
| Exception | Raised when |
|---|---|
KeyError |
Node ID doesn't exist |
ValueError |
Node is deleted, or invalid operation (e.g., delete root) |
RuntimeError |
Tree structure is frozen, or payloads are frozen |
Dependencies
- numpy (>=1.24)
Metadata
Release files for vcti-array-tree 1.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vcti_array_tree-1.4.0.tar.gz | 22.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vcti_array_tree-1.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.1 kB
Release files / vcti_array_tree-1.4.0.tar.gz
| Download URL | vcti_array_tree-1.4.0.tar.gz |
|---|---|
| Size | 22.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0e0e91de0b10dc0b25f57331a2719a717d7661c246bc72aa770b2753f445e1d7
|
|
BLAKE2b-256 checksum How to use checksums |
28b7b079437841e07551eab623fdc8937097eb381d790bf2fe59397397ee14de
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 6, 2026.
Transparency logRelease files / vcti_array_tree-1.4.0-py3-none-any.whl
| Download URL | vcti_array_tree-1.4.0-py3-none-any.whl |
|---|---|
| Size | 14.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e133e4d0a8cf6a2d014e65e866b3664d8862d63231c9fa65ba8a3f658f25f3be
|
|
BLAKE2b-256 checksum How to use checksums |
c4a662a636c725798b36bdd7712da471eb51cf4202c2c03679183cb998801423
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 6, 2026.
Transparency log