Repository: google/jax

Issue:
Class inheriting nametuple is not treated as a valid JAX Type
```
import collections
from jax.lax import scan
Point = collections.namedtuple(
    'Point',
    ['x', 'y']
)
class ZeroPoint(Point):
  def is_zero(self):
    return (x==0) and (y==0)
def loop(in_, idx):
  return in_
a = ZeroPoint(1., 0.)
b = ZeroPoint(1., 1.)
scan(loop, [a,b], np.arange(2))
```

The error was `TypeError: <class '__main__.ZeroPoint'> is not a valid Jax type`


Discussion:
A related note would be, a class which inherits pytree class may be considered as a valid pytree type as well?
So far we haven't followed the convention that a subclass of a pytree is a pytree. I think there are clear ways to implement that, but it's a bit more magic that can lead to less predictable behavior (plus potential overheads, though those would need to be profiled before we worry about them). So it's less clear whether we should add that complexity.

When there's an ambiguous decision like whether pytrees should follow subclassing, we usually take the conservative route and keep things the way they are (more explicit, less magic) until a concrete use case comes along, or we get enough evidence that we're really violating user expectations.

#506 explored one way to implement a pytree mechanism that follows subclassing, though I think it added significant complexity. If we decide to go that route, there might be simpler alternative mechanisms (like using `isinstance` instead of manually traversing the MRO).

We might want to separate out the the narrower case of namedtuple subclasses, though. Namedtuples were requested several times to act like pytrees by default (without having to explicitly register them), and users expected them to work like other Python builtins. But [our special handling for namedtuples](https://github.com/google/jax/blob/9dfe27880517d5583048e7a3384b504681968fb4/jax/tree_util.py#L250) only works for direct subclasses of `tuple` (like namedtuple classes are, but not their children). I think we could make namedtuple subclasses work just by writing `isinstance` there.

That would fix the original issue, but not address the broader question in the second comment. Is it worth handling all subclassing? Opinions welcome!

By the way, for posterity (since I know @zhongwen already knows this), as a workaround in the meantime you can always register your class as a pytree using `jax.tree_util.register_pytree_node`, and for this special case of a namedtuple you can use this utility by @sschoenholz:

```python
def register_pytree_namedtuple(cls):
  register_pytree_node(
      cls,
      lambda xs: (tuple(xs), None),
      lambda _, xs: cls(*xs))
```

In particular, you'd just have to import that function and write after your class definition:

```python
register_pytree_namedtuple(ZeroPoint)
```

As a tradeoff that requires more explicitness from the user but keeps the core system simpler and faster, that seems pretty good!
I forgot to say, thanks for opening this with such a clear and concise explanation!

Resolving patch:
diff --git a/jax/tree_util.py b/jax/tree_util.py
--- a/jax/tree_util.py
+++ b/jax/tree_util.py
@@ -247,7 +247,7 @@ def _get_node_type(maybe_tree):
   return node_types.get(t) or _namedtuple_node(t)
 
 def _namedtuple_node(t):
-  if t.__bases__ == (tuple,) and hasattr(t, '_fields'):
+  if issubclass(t, tuple) and hasattr(t, '_fields'):
     return NamedtupleNode
 
 NamedtupleNode = NodeType('namedtuple',


Test patch:
diff --git a/tests/api_test.py b/tests/api_test.py
--- a/tests/api_test.py
+++ b/tests/api_test.py
@@ -593,6 +593,23 @@ def f(pt):
     f_jit = api.jit(f)
     self.assertAllClose(f(pt), f_jit(pt), check_dtypes=False)
 
+  def test_namedtuple_subclass_transparency(self):
+    # See https://github.com/google/jax/issues/806
+    Point = collections.namedtuple("Point", ["x", "y"])
+
+    class ZeroPoint(Point):
+      def is_zero(self):
+        return (self.x == 0) and (self.y == 0)
+
+    pt = ZeroPoint(0., 0.)
+
+    def f(pt):
+      return 0. if pt.is_zero() else np.sqrt(pt.x ** 2 + pt.y ** 2)
+
+    f(pt)  # doesn't crash
+    g = api.grad(f)(pt)
+    self.assertIsInstance(pt, ZeroPoint)
+
   def test_eval_shape(self):
     def fun(x, y):
       return np.tanh(np.dot(x, y) + 3.)