Skip to content

Commit 0177df5

Browse files
committed
Address code review feedback on pure pytree utilities
- _is_leaf: exact type matching + explicit namedtuple detection; dict/list/ tuple subclasses are now leaves (JAX parity), fixing a MyTuple TypeError - docstrings: convert new pytree helpers to the codebase Sphinx convention - tests: consolidate scattered JAX-comparison classes into one section; remove redundant comments and incidental JAX citations - CHANGELOG: concise 'Add pure-Python pytree utilities for the abstract backend.' under Added (was 'Align', which implied prior existence)
1 parent 1d5ed1e commit 0177df5

3 files changed

Lines changed: 133 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020

2121
- Add support for OMECO contraction path finder.
2222

23+
- Add pure-Python pytree utilities for the abstract backend.
24+
2325
### Changed
2426

2527
- Replace hardcoded `np.complex128`/`np.complex64` with `cons.npdtype`/`dtypestr` in gates and translation modules, making the codebase respect `tc.set_dtype()`.
2628

27-
- Align pure pytree utilities (`_pure_tree_flatten`, `_pure_tree_unflatten`, `_pure_tree_map`) with JAX `tree_util` semantics, including `OrderedDict` (insertion-order) and `defaultdict` (sorted keys + `default_factory`) round-trip support.
28-
2929
### Fixed
3030

3131
- Fix `numpy_backend.cast()`, `jax_backend.cast()`, and `pytorch_backend.cast()` to correctly handle Python scalar inputs.

tensorcircuit/backends/abstract_backend.py

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818

1919
class _TreeDef:
20-
"""A JAX-compatible treedef that records pytree structure for unflattening."""
20+
"""
21+
A JAX-compatible treedef that records pytree structure for unflattening.
22+
"""
2123

2224
__slots__ = ("typ", "sorted_keys", "children", "num_leaves", "aux_data")
2325

@@ -29,15 +31,33 @@ def __init__(
2931
num_leaves: int,
3032
aux_data: Any = None,
3133
) -> None:
34+
"""
35+
Store the structure of one pytree node.
36+
37+
:param typ: Python type of this node (a container type, ``NoneType``
38+
for an empty pytree, or :class:`_LeafSentinel` for a leaf)
39+
:type typ: type
40+
:param sorted_keys: keys of a dict-like node in flatten order, else ``None``
41+
:type sorted_keys: Optional[Tuple[str, ...]]
42+
:param children: the :class:`_TreeDef` of each child node
43+
:type children: Tuple["_TreeDef", ...]
44+
:param num_leaves: total number of leaves contained in this subtree
45+
:type num_leaves: int
46+
:param aux_data: per-type payload such as a ``defaultdict`` factory,
47+
defaults to ``None``
48+
:type aux_data: Any
49+
"""
3250
self.typ = typ
33-
self.sorted_keys = sorted_keys # non-None only for dict like
51+
self.sorted_keys = sorted_keys
3452
self.children = children
3553
self.num_leaves = num_leaves
36-
self.aux_data = aux_data # only used by defaultdict (factory)
54+
self.aux_data = aux_data
3755

3856

3957
class _LeafSentinel:
40-
"""Sentinel type so that ``_TreeDef`` can represent a leaf node."""
58+
"""
59+
Sentinel type so that :class:`_TreeDef` can represent a leaf node.
60+
"""
4161

4262
pass
4363

@@ -47,17 +67,32 @@ class _LeafSentinel:
4767

4868

4969
def _is_leaf(obj: Any) -> bool:
50-
"""Check whether *obj* is a leaf managed by the pure-pytree machinery.
51-
52-
Everything that is not a recognised pytree container (dict, list, tuple,
53-
namedtuple, or ``None``) is a leaf. This deliberately matches JAX
54-
semantics for the types supported here.
70+
"""
71+
Check whether *obj* is a leaf managed by the pure-pytree machinery.
72+
73+
Uses **exact** type matching, mirroring JAX ``tree_util``: only the
74+
built-in container types (``dict``, ``OrderedDict``, ``defaultdict``,
75+
``list``, ``tuple``) and ``namedtuple`` classes are traversed. Arbitrary
76+
subclasses (e.g. ``class MyDict(dict)``) are **leaves** — in JAX a
77+
subclass must be explicitly registered as a pytree node to be traversed,
78+
so without registration the whole instance is a single leaf.
79+
80+
:param obj: any Python object to classify
81+
:type obj: Any
82+
:return: ``False`` for recognised containers and ``None``, ``True`` otherwise
83+
:rtype: bool
5584
"""
5685
if obj is None:
5786
return False
58-
if isinstance(obj, dict):
87+
t = type(obj)
88+
if t is dict or t is OrderedDict or t is defaultdict:
5989
return False
60-
if isinstance(obj, (list, tuple)):
90+
if t is list or t is tuple:
91+
return False
92+
# ``namedtuple`` (and its subclasses) are tuple subclasses carrying
93+
# ``_fields``; keep them as containers while plain tuple subclasses
94+
# such as ``class MyTuple(tuple)`` remain leaves.
95+
if isinstance(obj, tuple) and hasattr(obj, "_fields"):
6196
return False
6297
return True
6398

@@ -67,12 +102,18 @@ def _pure_tree_flatten(pytree: Any) -> Tuple[List[Any], _TreeDef]:
67102
Pure Python tree flattening with JAX-compatible semantics.
68103
69104
Key behaviors:
105+
70106
* ``None`` is an empty pytree (0 leaves), not a leaf.
71107
* ``dict`` and ``defaultdict`` keys are iterated in **sorted** order
72108
for deterministic leaf ordering. ``defaultdict`` preserves the
73109
``default_factory`` for round-trip.
74110
* ``OrderedDict`` keys are iterated in **insertion** order.
75111
* ``namedtuple`` is tracked as a separate type from plain ``tuple``.
112+
113+
:param pytree: the Python structure to flatten
114+
:type pytree: Any
115+
:return: the flat list of leaves and the :class:`_TreeDef` describing the structure
116+
:rtype: Tuple[List[Any], _TreeDef]
76117
"""
77118
if pytree is None:
78119
return [], _TreeDef(_NONE_TYPE, None, (), 0)
@@ -88,7 +129,9 @@ def _pure_tree_flatten(pytree: Any) -> Tuple[List[Any], _TreeDef]:
88129
else:
89130
# dict and defaultdict: sorted keys for determinism
90131
keys = tuple(sorted(pytree.keys()))
91-
aux_data = pytree.default_factory if actual_type is defaultdict else None
132+
aux_data = (
133+
pytree.default_factory if isinstance(pytree, defaultdict) else None
134+
)
92135
children = []
93136
for k in keys:
94137
child_leaves, child_td = _pure_tree_flatten(pytree[k])
@@ -107,7 +150,16 @@ def _pure_tree_flatten(pytree: Any) -> Tuple[List[Any], _TreeDef]:
107150

108151

109152
def _unflatten_from_iter(treedef: _TreeDef, leaf_iter: Iterator[Any]) -> Any:
110-
"""Recursively rebuild one node, consuming leaves from *leaf_iter*."""
153+
"""
154+
Recursively rebuild one node, consuming leaves from *leaf_iter*.
155+
156+
:param treedef: the structure to rebuild
157+
:type treedef: _TreeDef
158+
:param leaf_iter: iterator over leaves, consumed left to right
159+
:type leaf_iter: Iterator[Any]
160+
:return: the rebuilt pytree node
161+
:rtype: Any
162+
"""
111163
if treedef.typ is _LEAF:
112164
return next(leaf_iter)
113165
if issubclass(treedef.typ, dict):
@@ -136,12 +188,21 @@ def _unflatten_from_iter(treedef: _TreeDef, leaf_iter: Iterator[Any]) -> Any:
136188

137189

138190
def _pure_tree_unflatten(treedef: _TreeDef, leaves: List[Any]) -> Any:
139-
"""Rebuild a pytree from *treedef* and *leaves*.
191+
"""
192+
Rebuild a pytree from *treedef* and *leaves*.
140193
141194
Uses the :class:`_TreeDef` produced by :func:`_pure_tree_flatten`.
142195
The input *leaves* list is **not** mutated; a :class:`ValueError` is
143196
raised when the number of leaves does not match the structure
144197
recorded in *treedef* (either too few or too many).
198+
199+
:param treedef: the structure describing the target pytree
200+
:type treedef: _TreeDef
201+
:param leaves: the flat leaves to repack; not mutated
202+
:type leaves: List[Any]
203+
:return: the rebuilt pytree
204+
:rtype: Any
205+
:raises ValueError: if the number of leaves does not match *treedef*
145206
"""
146207
leaf_iter = iter(leaves)
147208
try:
@@ -162,7 +223,16 @@ def _pure_tree_unflatten(treedef: _TreeDef, leaves: List[Any]) -> Any:
162223

163224

164225
def _treedefs_compatible(treedef_a: _TreeDef, treedef_b: _TreeDef) -> bool:
165-
"""Check whether two treedefs have compatible structure for tree_map."""
226+
"""
227+
Check whether two treedefs have compatible structure for ``tree_map``.
228+
229+
:param treedef_a: the first treedef to compare
230+
:type treedef_a: _TreeDef
231+
:param treedef_b: the second treedef to compare
232+
:type treedef_b: _TreeDef
233+
:return: ``True`` if both treedefs share type, key order, and nesting
234+
:rtype: bool
235+
"""
166236
if treedef_a.typ is not treedef_b.typ:
167237
return False
168238
if treedef_a.typ is _LEAF:
@@ -191,6 +261,14 @@ def _pure_tree_map(f: Callable[..., Any], *pytrees: Any) -> Any:
191261
Applies *f* element-wise to corresponding leaves. All non-leaf
192262
arguments must share the **same** pytree structure (type, key order,
193263
and nesting); a :class:`ValueError` is raised on mismatch.
264+
265+
:param f: function applied to each tuple of corresponding leaves
266+
:type f: Callable[..., Any]
267+
:param pytrees: one or more pytrees with matching structure
268+
:type pytrees: Any
269+
:return: a pytree shaped like the inputs with ``f`` applied to the leaves
270+
:rtype: Any
271+
:raises ValueError: if the non-leaf arguments have mismatched structure
194272
"""
195273
if not pytrees:
196274
raise TypeError("_pure_tree_map requires at least one pytree argument")
@@ -199,7 +277,6 @@ def _pure_tree_map(f: Callable[..., Any], *pytrees: Any) -> Any:
199277
if all(_is_leaf(p) for p in pytrees):
200278
return f(*pytrees)
201279

202-
# Flatten, validate structure, and collect leaf lists in one pass
203280
flat_results = []
204281
first_treedef = None
205282
all_leaves: List[List[Any]] = []
@@ -216,9 +293,9 @@ def _pure_tree_map(f: Callable[..., Any], *pytrees: Any) -> Any:
216293

217294
assert first_treedef is not None
218295

219-
# Apply f to corresponding leaves
220296
for i in range(first_treedef.num_leaves):
221-
flat_results.append(f(*(ls[i] for ls in all_leaves)))
297+
corresponding_leaves = [leaves[i] for leaves in all_leaves]
298+
flat_results.append(f(*corresponding_leaves))
222299

223300
return _pure_tree_unflatten(first_treedef, flat_results)
224301

tests/test_pure_pytree.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def test_replace_values(self):
317317

318318

319319
class TestUnflattenLeafCountValidation:
320-
"""Validate leaf count on unflatten (JAX raises ValueError)."""
320+
"""Validate leaf count on unflatten."""
321321

322322
def test_extra_leaves_raises_valueerror(self):
323323
_, td = _pure_tree_flatten({"a": 1})
@@ -409,7 +409,7 @@ def test_three_args(self):
409409

410410

411411
class TestTreeMapStructureMismatch:
412-
"""JAX raises ValueError on structure mismatch."""
412+
"""Reject structure mismatches with ValueError."""
413413

414414
def test_scalar_broadcast_is_rejected(self):
415415
with pytest.raises(ValueError, match="structure mismatch"):
@@ -549,23 +549,46 @@ def test_frozenset_is_leaf(self):
549549
assert leaves == [fs]
550550
assert td.typ is _LEAF
551551

552-
def test_dict_subclass_is_container(self):
552+
def test_dict_subclass_is_leaf(self):
553553
class MyDict(dict):
554554
pass
555555

556556
tree = MyDict({"a": 1})
557557
leaves, td = _pure_tree_flatten(tree)
558-
assert leaves == [1]
558+
assert leaves == [tree]
559+
assert td.typ is _LEAF
559560
assert td.num_leaves == 1
560561

561-
def test_list_subclass_is_container(self):
562+
def test_list_subclass_is_leaf(self):
562563
class MyList(list):
563564
pass
564565

565566
tree = MyList([1, 2])
566567
leaves, td = _pure_tree_flatten(tree)
568+
assert leaves == [tree]
569+
assert td.typ is _LEAF
570+
assert td.num_leaves == 1
571+
572+
def test_tuple_subclass_is_leaf(self):
573+
class MyTuple(tuple):
574+
pass
575+
576+
tree = MyTuple((1, 2))
577+
leaves, td = _pure_tree_flatten(tree)
578+
assert leaves == [tree]
579+
assert td.typ is _LEAF
580+
assert td.num_leaves == 1
581+
582+
def test_namedtuple_subclass_is_container(self):
583+
class SubPoint(Point):
584+
pass
585+
586+
tree = SubPoint(1, 2)
587+
leaves, td = _pure_tree_flatten(tree)
567588
assert leaves == [1, 2]
568-
assert td.num_leaves == 2
589+
result = _pure_tree_unflatten(td, leaves)
590+
assert type(result) is SubPoint
591+
assert result == SubPoint(1, 2)
569592

570593

571594
class TestUnflattenCornerCases:
@@ -663,8 +686,13 @@ def boom(x):
663686
_pure_tree_map(boom, {"a": 1})
664687

665688

666-
class TestJAXComparisonTreeMap:
667-
"""Directly compare tree_map behaviour with JAX."""
689+
# ---------------------------------------------------------------------------
690+
# JAX cross-validation
691+
# ---------------------------------------------------------------------------
692+
693+
694+
class TestJAXComparison:
695+
"""Directly compare behaviour with JAX ``tree_util``."""
668696

669697
def test_tree_map_basic_matches_jax(self):
670698
tree = {"a": [1, 2], "b": 3}
@@ -696,10 +724,6 @@ def test_tree_map_rejects_none_vs_scalar_like_jax(self):
696724
with pytest.raises(ValueError):
697725
_pure_tree_map(lambda x, y: (x, y), None, 99)
698726

699-
700-
class TestJAXComparisonEmptyStructures:
701-
"""Compare empty structure handling with JAX."""
702-
703727
def test_empty_list_matches_jax(self):
704728
jax_leaves, _ = tree_util.tree_flatten([])
705729
pure_leaves, _ = _pure_tree_flatten([])
@@ -717,15 +741,6 @@ def test_roundtrip_empty_list(self):
717741
pure_td, pure_leaves
718742
)
719743

720-
721-
# ---------------------------------------------------------------------------
722-
# JAX comparison tests
723-
# ---------------------------------------------------------------------------
724-
725-
726-
class TestJAXComparison:
727-
"""Directly compare behaviour with JAX ``tree_util``."""
728-
729744
def test_flatten_dict_key_order_matches_jax(self):
730745
tree = {"b": 2, "a": 1}
731746
jax_leaves, _ = tree_util.tree_flatten(tree)

0 commit comments

Comments
 (0)