Repository: cbherbert/stochrare (language: python)

Issue:
Restrict specific 1D methods to 1D processes
Child issue of #4 

Some methods of the `DiffusionProcess` and derived classes can be restricted to uni-dimensional processes. An example is the Gillespie method that is currently only implemented for 1d dynamics.

A possible solution is to introduce a global decorator `one_d_method` that only calls the method if the 
dimension is correct.
```python
  def one_d_method(func):
      def wrapper_one_d_method(*args, **kwargs):
	  if dimension == 1:
	      func()
	  else:
	      raise NotImplementedError

  @one_d_method
  def _gillespie(x, w, dt, theta, D0):
      # ...
```


Discussion / hints:

Fix patch:
diff --git a/stochrare/dynamics/diffusion.py b/stochrare/dynamics/diffusion.py
index 447a8fd..4d71ca2 100644
--- a/stochrare/dynamics/diffusion.py
+++ b/stochrare/dynamics/diffusion.py
@@ -85,6 +85,17 @@ def diffusion(self):
     def diffusion(self, diffusionnew):
         self._diffusion = jit(diffusionnew, nopython=True)
 
+    @property
+    def dimension(self):
+        return self._dimension
+
+    @dimension.setter
+    def dimension(self, dimensionnew):
+        if dimensionnew < 1:
+            raise ValueError("Attribute dimension cannot be lower than 1")
+        self._dimension = dimensionnew
+
+
     def potential(self, X, t):
         """
         Compute the potential from which the force derives.
diff --git a/stochrare/utils.py b/stochrare/utils.py
index d615b3a..0c9649b 100644
--- a/stochrare/utils.py
+++ b/stochrare/utils.py
@@ -9,10 +9,13 @@
 This includes decorators, but is not restricted to it.
 
 .. autofunction:: pseudorand
+
+.. autofunction:: method1d
 """
 import functools
 import numpy as np
 
+
 def pseudorand(fun):
     """
     Decorator for methods of random objects.
@@ -24,10 +27,34 @@ def pseudorand(fun):
 
     The decorator will raise an error if the object does not have a `__deterministic__` attribute.
     """
+
     @functools.wraps(fun)
     def wrapper(*args, **kwargs):
         if args[0].__deterministic__:
             np.random.seed(100)
         retval = fun(*args, **kwargs)
         return retval
+
+    return wrapper
+
+
+def method1d(fun):
+    """
+    Decorator for methods of class DiffusionProcess.
+    If the object's `dimension` attribute is set to a value strictly
+    greater than 1, method is not executed and error is raised.
+    Else, method is executed as usual.
+    """
+
+    @functools.wraps(fun)
+    def wrapper(*args, **kwargs):
+        if args[0].dimension == 1:
+            retval = fun(*args, **kwargs)
+            return retval
+        else:
+            msg = "Method {} is not available for dynamics of dimension 1.".format(
+                fun.__name__
+            )
+            raise NotImplementedError(msg)
+
     return wrapper


Test patch:
diff --git a/stochrare/tests/test_diffusion.py b/stochrare/tests/test_diffusion.py
index e8f7951..e16abee 100644
--- a/stochrare/tests/test_diffusion.py
+++ b/stochrare/tests/test_diffusion.py
@@ -29,6 +29,9 @@ def test_properties(self):
         self.oup.D0 = 1
         self.oup.theta = 1
         self.oup.mu = 0
+        self.assertEqual(self.oup.dimension, 2)
+        with self.assertRaises(ValueError):
+            model = diffusion.DiffusionProcess(lambda x,t: x, lambda x,t: x, -1)
 
     def test_potential(self):
         x = np.linspace(-1, 1)
diff --git a/stochrare/tests/test_utils.py b/stochrare/tests/test_utils.py
index fed3f2d..d15616e 100644
--- a/stochrare/tests/test_utils.py
+++ b/stochrare/tests/test_utils.py
@@ -3,15 +3,38 @@
 """
 import unittest
 import numpy as np
-from stochrare.utils import pseudorand
+from stochrare.utils import pseudorand, method1d
+
 
 class TestUtils(unittest.TestCase):
     __deterministic__ = True
 
     @pseudorand
     def test_pseudorand(self):
-        results = np.array([0.54340494, 0.27836939, 0.42451759, 0.84477613, 0.00471886])
+        results = np.array(
+            [
+                0.54340494,
+                0.27836939,
+                0.42451759,
+                0.84477613,
+                0.00471886,
+            ]
+        )
         np.testing.assert_allclose(np.random.random(5), results, atol=1e-8)
 
+    def test_method1d(self):
+        class mock_class:
+            def __init__(self, dimension):
+                self.dimension = dimension
+
+            @method1d
+            def method(self):
+                return False
+
+        self.assertFalse(mock_class(1).method())
+        with self.assertRaises(NotImplementedError):
+            mock_class(2).method()
+
+
 if __name__ == "__main__":
     unittest.main()