|
| 1 | +from collections.abc import Callable |
| 2 | +from typing import Any, TypeVar, cast |
| 3 | + |
| 4 | +import optree |
| 5 | + |
| 6 | +from d9d.core.types import PyTree |
| 7 | + |
| 8 | +from .flatten import IsLeaf, PyTreeFlattener |
| 9 | + |
| 10 | +TLeaf = TypeVar("TLeaf") |
| 11 | +TMapped = TypeVar("TMapped") |
| 12 | +TTree = TypeVar("TTree", bound=PyTree) |
| 13 | + |
| 14 | +PyTreeSpec = optree.PyTreeSpec |
| 15 | + |
| 16 | + |
| 17 | +_flattener = PyTreeFlattener() |
| 18 | + |
| 19 | + |
| 20 | +def tree_flatten(tree: PyTree[TLeaf], is_leaf: IsLeaf | None = None) -> tuple[list[TLeaf], PyTreeSpec]: |
| 21 | + """Flattens a pytree into its leaves and a structure specification. |
| 22 | +
|
| 23 | + Args: |
| 24 | + tree: The nested structure to flatten. |
| 25 | + is_leaf: Optional predicate; when it returns ``True`` for a node, that node is kept as a leaf |
| 26 | + and not traversed further. |
| 27 | +
|
| 28 | + Returns: |
| 29 | + A tuple of the leaf list and a ``PyTreeSpec`` that can rebuild the structure via |
| 30 | + `tree_unflatten`. |
| 31 | + """ |
| 32 | + return _flattener.flatten(tree, is_leaf) |
| 33 | + |
| 34 | + |
| 35 | +def tree_unflatten(treespec: PyTreeSpec, leaves: list[TLeaf]) -> PyTree[TLeaf]: |
| 36 | + """Reconstructs a pytree from leaves and a structure specification. |
| 37 | +
|
| 38 | + Args: |
| 39 | + treespec: A specification produced by `tree_flatten`. |
| 40 | + leaves: The leaves to place into the structure, in flatten order. |
| 41 | +
|
| 42 | + Returns: |
| 43 | + The reconstructed nested structure. |
| 44 | + """ |
| 45 | + return optree.tree_unflatten(treespec, leaves) |
| 46 | + |
| 47 | + |
| 48 | +def tree_leaves(tree: PyTree[TLeaf], is_leaf: IsLeaf | None = None) -> list[TLeaf]: |
| 49 | + """Returns the leaves of a pytree in deterministic (sorted-key) order. |
| 50 | +
|
| 51 | + Args: |
| 52 | + tree: The nested structure to flatten. |
| 53 | + is_leaf: Optional predicate; when it returns ``True`` for a node, that node is kept as a leaf |
| 54 | + and not traversed further. |
| 55 | +
|
| 56 | + Returns: |
| 57 | + The list of leaves. |
| 58 | + """ |
| 59 | + return _flattener.flatten(tree, is_leaf)[0] |
| 60 | + |
| 61 | + |
| 62 | +def tree_map(func: Callable[[TLeaf], TMapped], tree: PyTree[TLeaf]) -> PyTree[TMapped]: |
| 63 | + """Applies ``func`` to every leaf of a pytree, returning a structurally-identical tree. |
| 64 | +
|
| 65 | + Args: |
| 66 | + func: The function to apply to each leaf. |
| 67 | + tree: The nested structure to map over. |
| 68 | +
|
| 69 | + Returns: |
| 70 | + A new tree with ``func`` applied to each leaf. |
| 71 | + """ |
| 72 | + leaves, treespec = _flattener.flatten(tree) |
| 73 | + return optree.tree_unflatten(treespec, [func(leaf) for leaf in leaves]) |
| 74 | + |
| 75 | + |
| 76 | +def tree_map_only( |
| 77 | + filters: type | tuple[type, ...], |
| 78 | + func: Callable[[Any], Any], |
| 79 | + tree: TTree, |
| 80 | +) -> TTree: |
| 81 | + """Applies ``func`` only to leaves that are instances of ``type_or_types``. |
| 82 | +
|
| 83 | + Leaves of any other type are returned unchanged. This is the common case for tensor |
| 84 | + operations over trees that also carry non-tensor bookkeeping (e.g. moving only tensors to a |
| 85 | + device while leaving strings and ints alone). The returned tree preserves the structure and |
| 86 | + leaf types of the input. |
| 87 | +
|
| 88 | + Args: |
| 89 | + filters: The leaf type(s) that ``func`` should be applied to. |
| 90 | + func: The function to apply to matching leaves. |
| 91 | + tree: The nested structure to map over. |
| 92 | +
|
| 93 | + Returns: |
| 94 | + A new tree with ``func`` applied to matching leaves only. |
| 95 | + """ |
| 96 | + leaves, treespec = _flattener.flatten(tree) |
| 97 | + mapped = [func(leaf) if isinstance(leaf, filters) else leaf for leaf in leaves] |
| 98 | + return cast(TTree, optree.tree_unflatten(treespec, mapped)) |
| 99 | + |
| 100 | + |
| 101 | +def tree_leaves_with_path(tree: PyTree[TLeaf], is_leaf: IsLeaf | None = None) -> list[tuple[tuple[Any, ...], TLeaf]]: |
| 102 | + """Returns ``(path, leaf)`` pairs for every leaf of a pytree. |
| 103 | +
|
| 104 | + Each path is a tuple of keys and indices reaching the leaf from the root: ``str`` for dict keys |
| 105 | + and dataclass field names, ``int`` for sequence indices. |
| 106 | +
|
| 107 | + Args: |
| 108 | + tree: The nested structure to flatten. |
| 109 | + is_leaf: Optional predicate; when it returns ``True`` for a node, that node is kept as a leaf |
| 110 | + and not traversed further. |
| 111 | +
|
| 112 | + Returns: |
| 113 | + A list of ``(path, leaf)`` tuples in deterministic (sorted-key) order. |
| 114 | + """ |
| 115 | + paths, leaves, _ = _flattener.flatten_with_path(cast(Any, tree), is_leaf) |
| 116 | + return list(zip(paths, leaves, strict=True)) |
0 commit comments