Metadata-Version: 2.4
Name: vcti-array-tree
Version: 1.3.0
Summary: Generic hierarchical tree backed by NumPy arrays with typed node payloads
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/AshDavid12/vcti-python-array-tree
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Classifier: Operating System :: OS Independent
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: hypothesis; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Requires-Dist: numpy; extra == "typecheck"
Dynamic: license-file

# 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** — `LazyDataNode` loads array data on demand, releases to free memory
- **Frozen mode** — prevent mutations after construction
- **Zero hard dependencies** beyond NumPy

## Installation

```bash
pip install vcti-array-tree>=1.3.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-array-tree>=1.3.0",
]
```

---

## Quick Start

```python
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

```python
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

```python
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()` | Make tree immutable |
| `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:

```python
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:

```python
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 is frozen and a mutation is attempted |

---

## Dependencies

- [numpy](https://numpy.org/) (>=1.24)
