Repository: pydata/sparse

Issue:
'COO' object has no attribute 'flatten' 
I am trying to compute the outer product between a dask array (a) and a COO sparse array (b) by calling `da.outer(a,b)`. It looks like it is using the correct protocol, I am just wondering if there is a better way to go about this? Also please let me know if I need to provide any more information.


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pydata/sparse/pull/317?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@77791e8`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.


```diff
@@            Coverage Diff            @@
##             master     #317   +/-   ##
=========================================
  Coverage          ?   93.94%           
=========================================
  Files             ?       19           
  Lines             ?     2311           
  Branches          ?        0           
=========================================
  Hits              ?     2171           
  Misses            ?      140           
  Partials          ?        0
```




Patch:
diff --git a/azure-pipelines-docs.yml b/azure-pipelines-docs.yml
deleted file mode 100644
index eef3bc5..0000000
--- a/azure-pipelines-docs.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-pool:
-  vmImage: 'ubuntu-16.04'
-
-steps:
-  - task: UsePythonVersion@0
-    inputs:
-      versionSpec: '3.7'
-      architecture: "x64"
-
-  - script: |
-      pip install -e .[docs]
-    displayName: Install package
-
-  - script: sphinx-build -W -b html docs/ _build/html
-    displayName: Build documentation
-
-  - task: PublishPipelineArtifact@0
-    inputs:
-      artifactName: 'Documentation'
-      targetPath: '$(System.DefaultWorkingDirectory)/_build/html'
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index e3c2278..46b7295 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -31,3 +31,19 @@ jobs:
     vmImage: 'vs2017-win2016'
   steps:
     - template: ci/azure-steps.yml
+- job: Docs
+  pool:
+    vmImage: 'ubuntu-16.04'
+  steps:
+    - template: ci/azure-docs.yml
+
+trigger:
+  branches:
+    include:
+    - master
+  tags:
+    include:
+    - '*'
+
+pr:
+- master
diff --git a/ci/azure-docs.yml b/ci/azure-docs.yml
new file mode 100644
index 0000000..5fa79c1
--- /dev/null
+++ b/ci/azure-docs.yml
@@ -0,0 +1,17 @@
+steps:
+- task: UsePythonVersion@0
+  inputs:
+    versionSpec: "3.7"
+    architecture: "x64"
+
+- script: |
+    pip install -e .[docs]
+  displayName: Install package
+
+- script: sphinx-build -W -b html docs/ _build/html
+  displayName: Build documentation
+
+- task: PublishPipelineArtifact@1
+  inputs:
+    artifactName: 'Documentation'
+    targetPath: '$(System.DefaultWorkingDirectory)/_build/html'
diff --git a/docs/generated/sparse.COO.flatten.rst b/docs/generated/sparse.COO.flatten.rst
new file mode 100644
index 0000000..c53d1c3
--- /dev/null
+++ b/docs/generated/sparse.COO.flatten.rst
@@ -0,0 +1,6 @@
+COO.flatten
+===========
+
+.. currentmodule:: sparse
+
+.. automethod:: COO.flatten
\ No newline at end of file
diff --git a/docs/generated/sparse.COO.rst b/docs/generated/sparse.COO.rst
index e33fc07..1250b13 100644
--- a/docs/generated/sparse.COO.rst
+++ b/docs/generated/sparse.COO.rst
@@ -72,6 +72,7 @@ COO
 
       COO.copy
       COO.dot
+      COO.flatten
       COO.reshape
       COO.resize
       COO.transpose
diff --git a/docs/generated/sparse.outer.rst b/docs/generated/sparse.outer.rst
new file mode 100644
index 0000000..437368b
--- /dev/null
+++ b/docs/generated/sparse.outer.rst
@@ -0,0 +1,6 @@
+outer
+=====
+
+.. currentmodule:: sparse
+
+.. autofunction:: outer
\ No newline at end of file
diff --git a/docs/generated/sparse.rst b/docs/generated/sparse.rst
index 95ed768..df86817 100644
--- a/docs/generated/sparse.rst
+++ b/docs/generated/sparse.rst
@@ -69,6 +69,8 @@ API
 
     ones_like
 
+    outer
+
     random
 
     result_type
diff --git a/sparse/_common.py b/sparse/_common.py
index 1808703..50f8fdf 100644
--- a/sparse/_common.py
+++ b/sparse/_common.py
@@ -412,3 +412,29 @@ def ones_like(a, dtype=None, format=None, compressed_axes=None):
         format=format,
         compressed_axes=compressed_axes,
     )
+
+
+def outer(a, b, out=None):
+    """
+    Return outer product of two sparse arrays.
+
+    Parameters
+    ----------
+    a, b : sparse.SparseArray
+        The input arrays.
+    out : sparse.SparseArray
+        The output array.
+    
+    Examples
+    --------
+    >>> import numpy as np
+    >>> import sparse
+    >>> a = sparse.COO(np.arange(4))
+    >>> o = sparse.outer(a, a)
+    >>> o.todense()
+    array([[0, 0, 0, 0],
+           [0, 1, 2, 3],
+           [0, 2, 4, 6],
+           [0, 3, 6, 9]])
+    """
+    return np.multiply.outer(a, b, out=out)
diff --git a/sparse/_coo/common.py b/sparse/_coo/common.py
index 206149b..3a6e156 100644
--- a/sparse/_coo/common.py
+++ b/sparse/_coo/common.py
@@ -1373,7 +1373,6 @@ def _diagonal_idx(coordlist, axis1, axis2, offset):
 
     offset : int
         Offset of the diagonal from the main diagonal. Defaults to main diagonal (0).
-
     """
     return np.array(
         [
diff --git a/sparse/_coo/core.py b/sparse/_coo/core.py
index ebd4766..bc5cc93 100644
--- a/sparse/_coo/core.py
+++ b/sparse/_coo/core.py
@@ -1659,6 +1659,17 @@ class COO(SparseArray, NDArrayOperatorsMixin):  # lgtm [py/missing-equals]
         if out is not None:
             kwargs["dtype"] = out[0].dtype
 
+        if method == "outer":
+            method = "__call__"
+
+            cum_ndim = 0
+            inputs_transformed = []
+            for inp in inputs:
+                inputs_transformed.append(inp[(Ellipsis,) + (None,) * cum_ndim])
+                cum_ndim += inp.ndim
+
+            inputs = tuple(inputs_transformed)
+
         if method == "__call__":
             result = elemwise(ufunc, *inputs, **kwargs)
         elif method == "reduce":
@@ -1712,6 +1723,32 @@ class COO(SparseArray, NDArrayOperatorsMixin):  # lgtm [py/missing-equals]
 
         return linear_loc(self.coords, self.shape)
 
+    def flatten(self, order="C"):
+        """
+        Returns a new :obj:`COO` array that is a flattened version of this array.
+
+        Returns
+        -------
+        COO
+            The flattened output array.
+
+        Notes
+        -----
+        The :code:`order` parameter is provided just for compatibility with
+        Numpy and isn't actually supported.
+
+        Examples
+        --------
+        >>> s = COO.from_numpy(np.arange(10))
+        >>> s2 = s.reshape((2, 5)).flatten()
+        >>> s2.todense()
+        array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+        """
+        if order not in {"C", None}:
+            raise NotImplementedError("The `order` parameter is not" "supported.")
+
+        return self.reshape(-1)
+
     def reshape(self, shape, order="C"):
         """
         Returns a new :obj:`COO` array that is a reshaped version of this array.


Test patch:
diff --git a/sparse/tests/test_coo.py b/sparse/tests/test_coo.py
index 101bde2..d24a45e 100644
--- a/sparse/tests/test_coo.py
+++ b/sparse/tests/test_coo.py
@@ -2208,3 +2208,14 @@ def test_result_type(t1, t2, func, data):
     assert func(sparse.COO(a), sparse.COO(b)) == expect
     assert func(a.dtype, sparse.COO(b)) == np.result_type(a.dtype, b)
     assert func(sparse.COO(a), b.dtype) == np.result_type(a, b.dtype)
+
+
+@pytest.mark.parametrize("in_shape", [(5, 5), 62, (3, 3, 3)])
+def test_flatten(in_shape):
+    s = sparse.random(in_shape, density=0.5)
+    x = s.todense()
+
+    a = s.flatten()
+    e = x.flatten()
+
+    assert_eq(e, a)