Repository: sdepy/sdepy

Issue:
Process methods 'pmin', 'pmax', 'tmin', 'tmax': bug in 'out' parameter handling
The ``out`` parameter of summary methods for the ``process`` class is expected to behave as the ``out`` parameter of the corresponding ``numpy.ndarray`` methods, to which it is actually passed through. In particular, its shape should match the shape of the resulting summarized process.

For the methods in question, a properly sized ``out`` array causes a ``ValueError`` to be raised in numpy, while omitting the leading or trailing one dimensional axis is in fact required. This behavior is in contrast with that of all other summary methods, and is regarded as a bug to be fixed.

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/sdepy/sdepy/pull/13?src=pr&el=h1) Report
> Merging [#13](https://codecov.io/gh/sdepy/sdepy/pull/13?src=pr&el=desc) into [master](https://codecov.io/gh/sdepy/sdepy/commit/b90667045d3d2480b39151152ee7ba4b429fec19?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/sdepy/sdepy/pull/13/graphs/tree.svg?width=650&token=sXNigQjlFY&height=150&src=pr)](https://codecov.io/gh/sdepy/sdepy/pull/13?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master      #13   +/-   ##
=======================================
  Coverage   91.58%   91.58%           
=======================================
  Files           7        7           
  Lines        1877     1877           
  Branches      272      272           
=======================================
  Hits         1719     1719           
  Misses         96       96           
  Partials       62       62
```


| [Impacted Files](https://codecov.io/gh/sdepy/sdepy/pull/13?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sdepy/infrastructure.py](https://codecov.io/gh/sdepy/sdepy/pull/13/diff?src=pr&el=tree#diff-c2RlcHkvaW5mcmFzdHJ1Y3R1cmUucHk=) | `88.75% <ø> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sdepy/sdepy/pull/13?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sdepy/sdepy/pull/13?src=pr&el=footer). Last update [b906670...7843e22](https://codecov.io/gh/sdepy/sdepy/pull/13?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/sdepy/infrastructure.py b/sdepy/infrastructure.py
index dc873b3..d557ea4 100644
--- a/sdepy/infrastructure.py
+++ b/sdepy/infrastructure.py
@@ -796,7 +796,8 @@ class process(np.ndarray):
         the minimum process value attained across paths.
         """
         return process(t=self.t,
-                       v=self.min(axis=-1, out=out))
+                       x=self.min(axis=-1, out=out,
+                                  keepdims=True))
 
     def pmax(self, out=None):
         """
@@ -804,7 +805,8 @@ class process(np.ndarray):
         the maximum process value attained across paths.
         """
         return process(t=self.t,
-                       v=self.max(axis=-1, out=out))
+                       x=self.max(axis=-1, out=out,
+                                  keepdims=True))
 
     def psum(self, dtype=None, out=None):
         """
@@ -855,14 +857,16 @@ class process(np.ndarray):
         process value attained along time.
         """
         return process(t=self.t[:1],
-                       x=self.min(axis=0, out=out)[np.newaxis, ...])
+                       x=self.min(axis=0, out=out,
+                                  keepdims=True))
 
     def tmax(self, out=None):
         """Constant process exposing for each path the maximum
         process value attained along time.
         """
         return process(t=self.t[:1],
-                       x=self.max(axis=0, out=out)[np.newaxis, ...])
+                       x=self.max(axis=0, out=out,
+                                  keepdims=True))
 
     def tsum(self, dtype=None, out=None):
         """


Test patch:
diff --git a/sdepy/tests/test_process.py b/sdepy/tests/test_process.py
index 26bea8d..5a7f2ad 100644
--- a/sdepy/tests/test_process.py
+++ b/sdepy/tests/test_process.py
@@ -582,28 +582,63 @@ def test_summary():
 
     # summary across paths
     for f in funcs:
+        # test values
         a = getattr(p, 'p' + f)()
-        b = getattr(np, f)(p, axis=-1)
-        assert_allclose(a, b[..., np.newaxis], rtol=eps(p.dtype))
-        a = p.pmean(dtype=np.float32)
+        b = getattr(np, f)(p, axis=-1)[..., np.newaxis]
+        assert_allclose(a, b, rtol=eps(p.dtype))
+        # test out parameter
+        y = np.full((3, 5, 7, 1), np.nan)
+        a = getattr(p, 'p' + f)(out=y)
+        assert_(a.base is y)
+        assert_allclose(y, b, rtol=eps(p.dtype))
+    for f in ('sum', 'mean', 'var', 'std'):
+        # test dtype parameter
+        a = getattr(p, 'p' + f)(dtype=np.float32)
         assert_(a.dtype == np.float32)
-        b = p.pvar(ddof=1, out=a)
-        assert_allclose(b, p.var(ddof=1, axis=-1)[..., np.newaxis],
-                        rtol=eps(np.float32))
+    # test ddof parameter for pvar and pstd
+    a = p.pvar(ddof=1)
+    assert_allclose(a, p.var(ddof=1, axis=-1)[..., np.newaxis],
+                    rtol=eps(p.dtype))
+    a = p.pstd(ddof=1)
+    assert_allclose(a, p.std(ddof=1, axis=-1)[..., np.newaxis],
+                    rtol=eps(p.dtype))
 
     # summary along the timeline
     for f in funcs:
+        # test values
         a = getattr(p, 't' + f)()
-        b = getattr(np, f)(p, axis=0)
-        assert_allclose(a, b[np.newaxis, ...], rtol=eps(p.dtype))
-        a = p.tmean(dtype=np.float32)
-        assert_(a.dtype == np.float32)
-        b = p.tvar(ddof=1, out=a)
-        assert_allclose(b, p.var(ddof=1, axis=0)[np.newaxis, ...],
-                        rtol=eps(np.float32))
-        a = p.tcumsum()
-        b = np.cumsum(p, axis=0)
+        b = getattr(np, f)(p, axis=0)[np.newaxis, ...]
         assert_allclose(a, b, rtol=eps(p.dtype))
+        # test out parameter
+        y = np.full((1, 5, 7, 11), np.nan)
+        a = getattr(p, 't' + f)(out=y)
+        assert_(a.base is y)
+        assert_allclose(y, b, rtol=eps(p.dtype))
+    for f in ('sum', 'mean', 'var', 'std'):
+        # test dtype parameter
+        a = getattr(p, 't' + f)(dtype=np.float32)
+        assert_(a.dtype == np.float32)
+
+    # test ddof parameter for tvar and tstd
+    a = p.tvar(ddof=1)
+    assert_allclose(a, p.var(ddof=1, axis=0)[np.newaxis, ...],
+                    rtol=eps(p.dtype))
+    a = p.tstd(ddof=1)
+    assert_allclose(a, p.std(ddof=1, axis=0)[np.newaxis, ...],
+                    rtol=eps(p.dtype))
+
+    # test tcumsum values
+    a = p.tcumsum()
+    b = np.cumsum(p, axis=0)
+    assert_allclose(a, b, rtol=3*eps(p.dtype))
+    # test tcumsum out parameter
+    y = np.full((3, 5, 7, 11), np.nan)
+    a = p.tcumsum(out=y)
+    assert_(a.base is y)
+    assert_allclose(y, b, rtol=3*eps(p.dtype))
+    # test tcumsum dtype parameter
+    a = p.tcumsum(dtype=np.float32)
+    assert_(a.dtype == np.float32)
 
     # numpy summary operations
     for f in funcs: