Repository: ELIFE-ASU/Neet

Issue:
Add an optional index argument to network update methods
This adds ability for asynchronous updating

Discussion:

Patch:
diff --git a/neet/automata.py b/neet/automata.py
index aaa9971..e94dfd7 100644
--- a/neet/automata.py
+++ b/neet/automata.py
@@ -182,7 +182,7 @@ class ECA(object):
 
         return True
 
-    def _unsafe_update(self, lattice):
+    def _unsafe_update(self, lattice, index=None):
         """
         Update the state of the ``lattice``, in place, without
         checking the validity of the arguments.
@@ -194,13 +194,25 @@ class ECA(object):
             >>> ca = ECA(30)
             >>> xs = [0,0,1,0,0]
             >>> ca._unsafe_update(xs)
+            [0, 1, 1, 1, 0]
             >>> xs
             [0, 1, 1, 1, 0]
             >>> ca.boundary = (0,1)
             >>> ca._unsafe_update(xs)
-            >>> xs
             [1, 1, 0, 0, 0]
 
+        ::
+
+            >>> ca = ECA(30)
+            >>> xs = [0,0,1,0,0]
+            >>> ca._unsafe_update(xs, index=1)
+            [0, 1, 1, 0, 0]
+            >>> xs
+            [0, 1, 1, 0, 0]
+            >>> ca.boundary = (0,1)
+            >>> ca._unsafe_update(xs, index=-1)
+            [0, 1, 1, 0, 1]
+
         ::
 
             >>> xs = [0,0,2,0,0]
@@ -210,6 +222,8 @@ class ECA(object):
 
         :param lattice: the one-dimensional sequence of states
         :type lattice: sequence
+        :param index: the index to update (or None)
+        :type index: int
         :returns: the updated lattice
         """
         if self.boundary:
@@ -219,15 +233,33 @@ class ECA(object):
             left  = lattice[-1]
             right = lattice[0]
         code = self.code
-        d = 2 * left + lattice[0]
-        for i in range(1, len(lattice)):
-            d = 7 & (2 * d + lattice[i])
-            lattice[i-1] = 1 & (code >> d)
-        d = 7 & (2 * d + right)
-        lattice[-1] = 1 & (code >> d)
+        if index is None:
+            d = 2 * left + lattice[0]
+            for i in range(1, len(lattice)):
+                d = 7 & (2 * d + lattice[i])
+                lattice[i-1] = 1 & (code >> d)
+            d = 7 & (2 * d + right)
+            lattice[-1] = 1 & (code >> d)
+        else:
+            if index < 0:
+                index += len(lattice)
+
+            if index == 0:
+                d = left
+            else:
+                d = lattice[index-1]
+
+            d = 2 * d + lattice[index]
+
+            if index + 1 == len(lattice):
+                d = 2 * d + right
+            else:
+                d = 2 * d + lattice[index+1]
+
+            lattice[index] = 1 & (code >> (7 & d))
         return lattice
 
-    def update(self, lattice):
+    def update(self, lattice, index=None):
         """
         Update the state of the ``lattice`` in place.
 
@@ -237,13 +269,13 @@ class ECA(object):
 
             >>> ca = ECA(30)
             >>> xs = [0,0,1,0,0]
-            >>> ca.update(xs)
+            >>> ca.update(xs, index=1)
+            [0, 1, 1, 0, 0]
             >>> xs
-            [0, 1, 1, 1, 0]
-            >>> ca.boundary = (0,1)
-            >>> ca.update(xs)
-            >>> xs
-            [1, 1, 0, 0, 0]
+            [0, 1, 1, 0, 0]
+            >>> ca.boundary = (1,1)
+            >>> ca.update(xs, index=-1)
+            [0, 1, 1, 0, 1]
 
         ::
 
@@ -257,13 +289,22 @@ class ECA(object):
             Traceback (most recent call last):
                 ...
             ValueError: invalid value "2" in lattice
+            >>> ca.update(xs, index=5)
+            Traceback (most recent call last):
+                  ...
+            IndexError: list index out of range
 
         :param lattice: the one-dimensional sequence of states
         :type lattice: sequence
+        :param index: the index to update (or None)
+        :type index: int
         :returns: the updated lattice
         :raises ValueError: if ``lattice`` is empty
         :raises TypeError: if ``lattice`` is not iterable
         :raises ValueError: unless :math:`lattice[i] \in \{0,1\}` for all :math:`i`
+        :raises IndexError: if ``index is not None and index > len(states)``
         """
         ECA.check_lattice(lattice)
-        return self._unsafe_update(lattice)
+        if index is not None and index < -len(lattice):
+            raise(IndexError("lattice index out of range"))
+        return self._unsafe_update(lattice, index)


Test patch:
diff --git a/test/test_automata.py b/test/test_automata.py
index fcee65b..52d3cf0 100644
--- a/test/test_automata.py
+++ b/test/test_automata.py
@@ -260,3 +260,51 @@ class TestECA(unittest.TestCase):
         eca.update(lattice)
         self.assertTrue(np.array_equal([1,1,0,0,0], lattice))
 
+
+    def test_update_index_error(self):
+        eca = ca.ECA(30)
+        with self.assertRaises(IndexError):
+            eca.update([0,0], index=2)
+
+        with self.assertRaises(IndexError):
+            eca.update([0,0], index=-3)
+
+
+    def test_update_index(self):
+        eca = ca.ECA(30, (1,1))
+
+        lattice = [0,0,0,0,0]
+        eca.update(lattice, index=0)
+        self.assertEqual([1,0,0,0,0], lattice)
+
+        lattice = [0,0,0,0,0]
+        eca.update(lattice, index=1)
+        self.assertEqual([0,0,0,0,0], lattice)
+
+        lattice = [0,0,0,0,0]
+        eca.update(lattice, index=-1)
+        self.assertEqual([0,0,0,0,1], lattice)
+
+        lattice = [0,0,1,0,0]
+        eca.update(lattice, index=1)
+        self.assertEqual([0,1,1,0,0], lattice)
+
+
+    def test_update_index_numpy(self):
+        eca = ca.ECA(30, (1,1))
+
+        lattice = np.asarray([0,0,0,0,0])
+        eca.update(lattice, index=0)
+        self.assertTrue(np.array_equal([1,0,0,0,0], lattice))
+
+        lattice = np.asarray([0,0,0,0,0])
+        eca.update(lattice, index=1)
+        self.assertTrue(np.array_equal([0,0,0,0,0], lattice))
+
+        lattice = np.asarray([0,0,0,0,0])
+        eca.update(lattice, index=-1)
+        self.assertTrue(np.array_equal([0,0,0,0,1], lattice))
+
+        lattice = np.asarray([0,0,1,0,0])
+        eca.update(lattice, index=1)
+        self.assertTrue(np.array_equal([0,1,1,0,0], lattice))