diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b9e544a..d0a64d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,6 +41,11 @@ jobs: pip install suitesparse-graphblas==7.4.4.1a1 pip install python-graphblas + - name: Install pydata sparse + if: ${{ !contains(matrix.python-version, 'pypy') && matrix.python-version != '3.7' }} # no wheels for pypy and old python + run: | + pip install sparse + - name: Test without Jupyter if: ${{ !contains(matrix.python-version, 'pypy') }} # no scipy wheels for pypy run: pytest diff --git a/README.md b/README.md index ff152a8..47a9783 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Sparse matrix spy plot and sparkline renderer. Supports: * **SciPy** - sparse matrices and arrays like `csr_matrix` and `coo_array` [(demo)](demo.ipynb) * **NumPy** - `ndarray` [(demo)](demo-numpy.ipynb) * **[Python-graphblas](https://github.com/python-graphblas/python-graphblas)** - `gb.Matrix` [(demo)](demo-python-graphblas.ipynb) +* **[PyData/Sparse](https://sparse.pydata.org/)** - `COO`, `DOK`, `GCXS` [(demo)](demo-pydata-sparse.ipynb) Features: * Simple `spy()` method, similar to MatLAB's spy. diff --git a/demo-pydata-sparse.ipynb b/demo-pydata-sparse.ipynb new file mode 100644 index 0000000..bc079dd --- /dev/null +++ b/demo-pydata-sparse.ipynb @@ -0,0 +1,111 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "jupyter": { + "source_hidden": true + }, + "ExecuteTime": { + "end_time": "2023-08-31T04:16:28.689682Z", + "start_time": "2023-08-31T04:16:28.275270Z" + } + }, + "outputs": [], + "source": [ + "import sparse" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "jupyter": { + "source_hidden": true + }, + "ExecuteTime": { + "end_time": "2023-08-31T04:16:28.739002Z", + "start_time": "2023-08-31T04:16:28.693626Z" + } + }, + "outputs": [], + "source": [ + "import scipy\n", + "A = sparse.COO.from_scipy_sparse(scipy.io.mmread(\"doc/matrices/email-Eu-core.mtx.gz\")).asformat(\"csr\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "ExecuteTime": { + "end_time": "2023-08-22T23:04:28.653403Z", + "start_time": "2023-08-22T23:04:28.580379Z" + } + }, + "source": [ + "\n", + "Now view the entire matrix as a spy plot:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "ExecuteTime": { + "end_time": "2023-08-31T04:16:30.310280Z", + "start_time": "2023-08-31T04:16:28.744808Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYEAAAFgCAYAAABdQcUDAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAB6MElEQVR4nO2dfZyVc/7/X9PdTFPNZJpmpqhUQpFqSxmFUrpvSbu0Gw2/Ct1QsqJCQjeya8td2LVYcrOskC8qIqxUontCotAU0kyF7ubz++P0Oua853z6XNc5Z27Omffz8ehxZq5zXZ+b65zper0/n/dNkjHGQFEURamUVCnvASiKoijlhz4EFEVRKjH6EFAURanE6ENAURSlEqMPAUVRlEqMPgQURVEqMfoQUBRFqcToQ0BRFKUSow8BRVGUSow+BJSEZe/evRg+fDhycnKQlJSEcePG4auvvkJSUhIee+yx8h5eheWyyy7D8ccfX97DUMoIXw+BvXv3YsqUKejduzcyMjKcf0yffPIJevfujdq1ayMjIwOXXnopvv/++xLnFRUVYdasWWjatClSUlJw2mmn4emnny5x3mWXXYakpKQS/04++WQ/06hU83vhhRdw8cUXo1mzZkhNTcVJJ52E6667Drt37y5x7vHHHx+2/6uuuirkvMceeyzseUlJScjPzw+e9/bbb1vPS0pKwrRp04Lnbt++HTfeeCO6deuGOnXqICkpCW+//bbneV522WXo2rVryLHp06fjsccew8iRI/HEE0/g0ksv9dxeNGzcuBG33norvvrqq7DvFxUVYe7cuWjbti1q1qyJevXq4dxzz8WaNWusbc6bNw9JSUmoXbt2ife6du2Kyy67LEajTzweeOABfegfhWp+Tv7hhx9w2223oXHjxmjTps1R/0i/+eYbnH322UhPT8f06dOxd+9e/PWvf8W6deuwYsUK1KhRI3ju5MmTMXPmTIwYMQKnn346XnrpJfz5z39GUlISBg8eHNJucnIy/vnPf4YcS09P9zONSjW/K664Ag0bNsQll1yCxo0bY926dbjvvvvw6quv4qOPPkLNmjVDzm/bti2uu+66kGMnnnhi2LZvu+02NG3aNORY3bp1gz+3bNkSTzzxRInrnnjiCSxatAg9e/YMHtu0aRPuvPNOtGjRAq1bt8ayZcv8TrUES5YswRlnnIEpU6YEj9n+Y44lGzduxNSpU9G1a9ewivr//b//h3nz5mHo0KEYM2YM9u3bh48//hg7d+4M297evXsxYcIE1KpVq5RHnpg88MADyMzM1AelDeODX3/91Wzfvt0YY8zKlSsNAPPoo4+GPXfkyJGmZs2a5uuvvw4eW7x4sQFgHnrooeCxb775xlSvXt2MHj06eKyoqMicddZZ5rjjjjOHDh0KHs/LyzO1atXyPNZ///vf1vdffvllk5+fH7fz88pbb71V4tjjjz9uAJh//OMfIcebNGli+vXr52zz0UcfNQDMypUrIxrTCSecYFq0aBFyrLCw0Pz444/GGGOee+45AyDs2G3k5eWZc845J+RY06ZNS8xny5YtR/1cY8HRxv/ss88aAOaFF17w3N4NN9xgTjrpJDNkyJCw349zzjnH5OXlRTHiUPLy8kyTJk1i1l55c8opp5T4bii/4Ws5KDk5GTk5OZ7O/e9//4v+/fujcePGwWM9evTAiSeeiP/85z/BYy+99BIOHjyIUaNGBY8lJSVh5MiR+Oabb8IqwsOHD6OwsPCo/T///PMYOnQo7rnnnhLvLVy4EIMGDcLf/va3uJ2fV+QSCQAMHDgQQGA5KxwHDhzAvn37PLW/Z88eHD582PN4VqxYgS+++AJDhgwJOV6nTh1kZGR4budocBlqy5Yt+L//+7/g8tPRrIAlS5bgrLPOQq1atVC3bl2cf/75Je7P119/jVGjRuGkk04KLuP88Y9/DGn3sccewx//+EcAQLdu3YJ906q8++670bFjRwwcOBBFRUXO+/z555/j73//O+6++25Uq+bLcA/Lk08+ifbt26NmzZrIyMjA4MGDsW3bNud1RUVFmD17Nk455RSkpKQgOzsbV155JX766aeQ844//nj0798fb7/9Njp06ICaNWuidevWwfm/8MILaN26NVJSUtC+fXt8/PHHJfr69NNP8Yc//AEZGRlISUlBhw4d8PLLL4ecwyXJ//3vfxg/fjzq16+PWrVqYeDAgSFLsscffzw2bNiApUuXBj8L/k0cPHgQU6dORYsWLZCSkoJ69eqhS5cuWLx4sc+7GuCXX37BNddcg8zMTNSpUwe///3v8e233yIpKQm33npryLnffvsthg0bhoYNGyI5ORlNmzbFyJEjceDAARhj0K1bN9SvXz/EOjxw4ABat26N5s2bB783e/bswbhx43D88ccjOTkZWVlZOO+88/DRRx95HnepbAx/++232LlzJzp06FDivY4dO4Z88B9//DFq1aqFli1bljiP7xfn559/RlpaGtLT05GRkYHRo0dj7969JfoZMmQIRo0ahXHjxuHf//538Pj777+PCy+8EGeddRZuv/32uJ1fNHDdPjMzs8R7S5YsQWpqKmrXro3jjz8ec+bMsbbTrVs3pKWlITU1Fb///e/x+eefO/ueN28eAJR4CMQSLkNlZmaibdu2eOKJJ/DEE0+gfv36Yc9/44030KtXL+zcuRO33norxo8fj/fffx+dO3cO+Q9+5cqVeP/99zF48GDcc889uOqqq/Dmm2+ia9eu+PnnnwEAZ599Nq655hoAwKRJk4J9t2zZEoWFhVixYgVOP/10TJo0Cenp6ahduzaaNWsWIhyKM27cOHTr1g19+/aN+r5MmzYNQ4cORYsWLXD33Xdj3LhxePPNN3H22WeH3SMqzpVXXonrr78enTt3xpw5c3D55Zdj3rx56NWrFw4ePBhy7hdffIE///nPGDBgAGbMmIGffvoJAwYMwLx583DttdfikksuwdSpU7F582ZcdNFFKCoqCl67YcMGnHHGGfjkk09w44034m9/+xtq1aqFCy64APPnzy8xrquvvhpr1qzBlClTMHLkSCxYsABjxowJvj979mwcd9xxOPnkk4OfxeTJkwEAt956K6ZOnYpu3brhvvvuw+TJk9G4cWNf/4EW57LLLsO9996Lvn374s4770TNmjXRr1+/Eud999136NixI5555hlcfPHFuOeee3DppZdi6dKl+Pnnn5GUlIR//etf+PXXX0P246ZMmYINGzbg0UcfDS4NXnXVVZg7dy4GDRqEBx54AH/5y19Qs2ZNq8ALS6QmxNGWS/heuOWY66+/3gAwv/76qzHGmH79+plmzZqVOG/fvn0GgLnxxhuDx2688UZzww03mGeffdY8/fTTJi8vzwAwnTt3NgcPHizRxuHDh82f/vQnU61aNfPSSy+ZtWvXmmOOOcacfvrpZs+ePXE/v0gZNmyYqVq1qvnss89Cjg8YMMDceeed5sUXXzSPPPKIOeusswwAM2HChJDznn32WXPZZZeZxx9/3MyfP9/cdNNNJjU11WRmZpqtW7da+z106JDJzs42HTt2POr4IlkOCke45a1wy0Ft27Y1WVlZweUoY4xZs2aNqVKlihk6dGjw2M8//1yij2XLlpX4LtjG/9FHHxkApl69eiY7O9s88MADZt68eaZjx44mKSnJvPbaayHnv/LKK6ZatWpmw4YNxpjolgu/+uorU7VqVTNt2rSQ4+vWrTPVqlULOS6Xg959910DwMybNy/k2tdff73E8SZNmhgA5v333w8eW7hwoQFQYvn0oYceKnGfunfvblq3bh38+zEmsHx65plnhiwhckmyR48epqioKHj82muvNVWrVjW7d+8OHrMtB7Vp08bT8qcXVq1aZQCYcePGhRy/7LLLDAAzZcqU4LGhQ4eaKlWqhF1OLT4X3p8nn3zSfPDBB6Zq1aol2k9PTw9Zao6EUnkIvPPOOwaAefbZZ0u8d/PNNxsA5qeffjLGGHPuueeali1bljjv8OHDBoAZO3bsUccxbdo0A8A8/fTTYd8/cOCA6dOnj0lJSTFZWVmmZcuW5ocffkiY+fll3rx5Yf9jD0dRUZHp1auXqVatmtm2bdtRz3333XdNUlKSufLKK63n8D+DOXPmHLWtsnwIfPfdd9b70atXL5OZmRm27QMHDpgffvjBfP/996Zu3bohf5y28fN7A8B88MEHweN79uwxmZmZpnPnzsFj+/fvNy1atDBjxowJHovmIXD33XebpKQk8/nnn5vvv/8+5F/Lli1Njx49Qvop/hC45pprTHp6utm5c2eJa2vXrm2GDx8ePLdJkyamVatWIX3v3r3bACjxWaxevdoAMI888ogxxpgff/zRJCUlmdtvv71EP1OnTjUAzDfffGOM+e0h8J///CekzRdeeMEAMGvWrAkesz0EzjnnHHP88ceXEEORwL9T2RYfDnwIHD582KSlpZnzzz/fU7u9evUyxxxzjGnRooU58cQTSwiRJk2amA4dOphvv/024rGXynIQPU72799f4r1ff/015JyaNWt6Os/GtddeiypVquCNN94I+3716tUxZ84cHDx4EDt37sS0adNQr14975MJQ0Wanx/effddDBs2DL169Qpxz7SRlJSEa6+9FocOHXK6a3bp0gWdOnU66jjnzZuHqlWr4uKLL/Y79FLj66+/BgCcdNJJJd5r2bIlfvjhh+D66y+//IJbbrkFjRo1QnJyMjIzM1G/fn3s3r0bBQUFzr74WTdt2hSdOnUKHq9duzYGDBiAFStW4NChQwCAv//97/jhhx8wderUqOcIBPYWjDFo0aIF6tevH/Lvk08+sXom8dqCggJkZWWVuHbv3r0lri2+Twb85t3WqFGjsMe5r/DFF1/AGIObb765RD/08HL1dcwxx4S0eTRuu+027N69GyeeeCJat26N66+/HmvXrnVeF46vv/4aVapUKeEtd8IJJ4T8/v3336OwsBCnnnqqp3YfeeQR/Pzzz/j888/x2GOPlfj/YtasWVi/fj0aNWqEjh074tZbb8WXX37pa+zR7zSFoUGDBgACvt+S7du3IyMjA8nJycFz33rrLRhjkJSUFHIeADRs2PCofXGDbteuXWHf//HHH3HBBRcgMzMTOTk5GDFiBE466SS0atUqorlxzMXHWJyynp9X1qxZg9///vc49dRT8fzzz3veZOQfrpf+GzVqhE2bNoV975dffsH8+fPRo0cPZGdnex94BeLqq6/Go48+inHjxiE3Nxfp6elBN9/i69o2+FmHm39WVhYOHjwYfODccccdGDVqFAoLC4NOAnv37oUxBl999RVSU1ORlZXleexFRUVISkrCa6+9hqpVq5Z4P1z8QfFrs7Kygvs5ErnXEq79ox03Ryrc8h7+5S9/Qa9evcKeK/9TdbV5NM4++2xs3rwZL730EhYtWoR//vOf+Pvf/44HH3wQw4cPd15fFrz99ttBEblu3Trk5uaGvH/RRRfhrLPOwvz587Fo0SLcdddduPPOO/HCCy+gT58+nvoolYfAsccei/r16+PDDz8s8d6KFSvQtm3b4O9t27bFP//5T3zyySch/zEvX748+P7R2LNnD3744Yewm3579+5F37598d1332Hp0qVo0KABunTpgvPOOw//+9//Io6KrCjz88rmzZvRu3dvZGVl4dVXXz3qH7yEqsJL/19++aX1vJdffhl79uwp1Q3hSGjSpAkAhH14ffrpp8jMzAxuwj3//PPIy8sL8Sr79ddfS2yqFn/YF6dhw4bIycnBt99+W+K97777DikpKahTpw62bt2KvXv3YtasWZg1a1aJc5s2bYrzzz8fL774otdponnz5jDGoGnTpta4j6Nd+8Ybb6Bz585OyzUamjVrBiBgvffo0SNm7do+DwDIyMjA5Zdfjssvvxx79+7F2WefjVtvvdX3Q6BJkyYoKirCli1b0KJFi+DxL774IuS8+vXrIy0tDevXr3e2uX37dlx99dXo2bMnatSoEXw48jtLGjRogFGjRmHUqFHYuXMnfve732HatGmeHwKlljZi0KBBeOWVV0Lcz95880189tlnQRc6ADj//PNRvXp1PPDAA8Fjxhg8+OCDOPbYY3HmmWcCCPyx7dmzp0Q/t99+O4wx6N27d8jx/fv34/zzz8eGDRvw6quv4rTTTkP9+vWxePFiVK1aFeedd15IdGu8zc8r+fn56NmzJ6pUqYKFCxda/5PetWtXCVfPgwcPYubMmahRowa6desWPB4uKvrVV1/FqlWrrON86qmnkJqaGnRPrSg0aNAAbdu2xeOPPx7yn/n69euxaNGiEK+cqlWrllCY9957b4n7xodGOI+biy++GNu2bQtxQ/zhhx/w0ksv4dxzz0WVKlWQlZWF+fPnl/jXrVs3pKSkYP78+Zg4caKveV544YWoWrUqpk6dWmIOxhj8+OOP1msvuugiHD58OKw33aFDh5yeRV7JyspC165d8dBDD4W1ssN977xQq1atsGOUc65duzZOOOGEsMu3Lmi5FP87BwLfj+JUqVIFF1xwARYsWBBWRBb/bEaMGIGioiI88sgjePjhh1GtWjUMGzYseM7hw4dLLENmZWWhYcOGvubg2xK47777sHv3bnz33XcAgAULFuCbb74BEDCXuc43adIkPPfcc+jWrRvGjh2LvXv34q677kLr1q1x+eWXB9s77rjjMG7cONx11104ePAgTj/9dLz44ot49913g2vIQOA/s3bt2uFPf/pTMI3CwoUL8eqrr6J37944//zzQ8b5/PPP47333sOCBQtCTKjGjRtj0aJFOOuss3D33XeXUFrxMj9aMa4I2N69e+PLL7/EhAkT8N577+G9994LvpednY3zzjsPQECp33HHHfjDH/6Apk2bYteuXXjqqaewfv16TJ8+PSR+4swzz0S7du3QoUMHpKen46OPPsK//vUvNGrUCJMmTSoxhl27duG1117DoEGDjmqF3HHHHQACboJAILKY473pppuOOs9ouOuuu9CnTx/k5uZi2LBh+OWXX3DvvfciPT09xL+7f//+eOKJJ5Ceno5WrVph2bJleOONN0rsMbVt2xZVq1bFnXfeiYKCAiQnJ+Pcc89FVlYWJk6ciP/85z8YNGgQxo8fj/T0dDz44IM4ePAgpk+fDgBITU3FBRdcUGKcL774IlasWBH2PRfNmzfHHXfcgYkTJ+Krr77C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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matspy import spy\n", + "\n", + "spy(A)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "outputs": [], + "source": [], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-08-31T04:16:30.312084Z", + "start_time": "2023-08-31T04:16:30.311101Z" + } + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/matspy/__init__.py b/matspy/__init__.py index 8ef96d7..bc4cae4 100644 --- a/matspy/__init__.py +++ b/matspy/__init__.py @@ -131,6 +131,9 @@ def _register_bundled(): from .adapters.graphblas_driver import GraphBLASDriver register_driver(GraphBLASDriver) + from .adapters.sparse_driver import PyDataSparseDriver + register_driver(PyDataSparseDriver) + _register_bundled() diff --git a/matspy/adapters/sparse_driver.py b/matspy/adapters/sparse_driver.py new file mode 100644 index 0000000..4e89a22 --- /dev/null +++ b/matspy/adapters/sparse_driver.py @@ -0,0 +1,18 @@ +# Copyright (C) 2023 Adam Lugowski. +# Use of this source code is governed by the BSD 2-clause license found in the LICENSE.txt file. +# SPDX-License-Identifier: BSD-2-Clause + +from typing import Any, Iterable + +from . import Driver, MatrixSpyAdapter + + +class PyDataSparseDriver(Driver): + @staticmethod + def get_supported_type_prefixes() -> Iterable[str]: + return ["sparse."] + + @staticmethod + def adapt_spy(mat: Any) -> MatrixSpyAdapter: + from .sparse_impl import PyDataSparseSpy + return PyDataSparseSpy(mat) diff --git a/matspy/adapters/sparse_impl.py b/matspy/adapters/sparse_impl.py new file mode 100644 index 0000000..2682c57 --- /dev/null +++ b/matspy/adapters/sparse_impl.py @@ -0,0 +1,66 @@ +# Copyright (C) 2023 Adam Lugowski. +# Use of this source code is governed by the BSD 2-clause license found in the LICENSE.txt file. +# SPDX-License-Identifier: BSD-2-Clause + +from typing import Tuple + +import numpy as np +import sparse + +from . import describe, generate_spy_triple_product, MatrixSpyAdapter + + +def generate_spy_triple_product_sparse(matrix_shape, spy_shape) -> Tuple[sparse.SparseArray, sparse.SparseArray]: + # construct a triple product that will scale the matrix + left, right = generate_spy_triple_product(matrix_shape, spy_shape) + + left_shape, (left_rows, left_cols) = left + right_shape, (right_rows, right_cols) = right + left_mat = sparse.COO(coords=(left_rows, left_cols), data=np.ones(len(left_rows)), shape=left_shape) + right_mat = sparse.COO(coords=(right_rows, right_cols), data=np.ones(len(right_rows)), shape=right_shape) + + return left_mat, right_mat + + +class PyDataSparseSpy(MatrixSpyAdapter): + def __init__(self, mat): + super().__init__() + self.mat = mat + + def get_shape(self) -> tuple: + return self.mat.shape + + def describe(self) -> str: + parts = [ + self.mat.format, + ] + + return describe(shape=self.mat.shape, + nnz=self.mat.nnz, nz_type=self.mat.dtype, + notes=", ".join(parts)) + + def get_spy(self, spy_shape: tuple) -> np.array: + if isinstance(self.mat, sparse.DOK): + self.mat = self.mat.asformat("coo") + + # construct a triple product that will scale the matrix + left, right = generate_spy_triple_product_sparse(self.mat.shape, spy_shape) + + # save existing matrix data + mat_data_save = self.mat.data + + # replace with all ones + self.mat.data = np.ones(self.mat.data.shape) + + # triple product + try: + spy = left @ self.mat @ right + except ValueError: + # broken matmul on some types + temp = self.mat.asformat("coo") + spy = left @ temp @ right + + # restore original matrix data + self.mat.data = mat_data_save + + return np.array(spy.todense()) diff --git a/tests/test_sparse.py b/tests/test_sparse.py new file mode 100644 index 0000000..14a6277 --- /dev/null +++ b/tests/test_sparse.py @@ -0,0 +1,58 @@ +# Copyright (C) 2023 Adam Lugowski. +# Use of this source code is governed by the BSD 2-clause license found in the LICENSE.txt file. +# SPDX-License-Identifier: BSD-2-Clause + +import unittest + +try: + import sparse +except ImportError: + sparse = None + +import numpy as np +import scipy.sparse + +from matspy import spy_to_mpl, to_sparkline, to_spy_heatmap + +np.random.seed(123) + + +@unittest.skipIf(sparse is None, "pydata/sparse not installed") +class PyDataSparseTests(unittest.TestCase): + def setUp(self): + self.mats = [ + sparse.COO.from_scipy_sparse(scipy.sparse.random(10, 10, density=0.4)), + sparse.COO.from_scipy_sparse(scipy.sparse.random(5, 10, density=0.4)), + sparse.COO.from_scipy_sparse(scipy.sparse.random(5, 1, density=0.4)), + sparse.COO.from_scipy_sparse(scipy.sparse.coo_matrix(([], ([], [])), shape=(10, 10))), + ] + + def test_no_crash(self): + import matplotlib.pyplot as plt + for fmt in "coo", "gcxs", "dok", "csr", "csc": + for source_mat in self.mats: + mat = source_mat.asformat(fmt) + + fig, ax = spy_to_mpl(mat) + plt.close(fig) + + res = to_sparkline(mat) + self.assertGreater(len(res), 10) + + def test_count(self): + arrs = [ + (0, sparse.COO(np.array([[0]]))), + (1, sparse.COO(np.array([[1]]))), + (0, sparse.COO(np.array([[0, 0], [0, 0]]))), + (1, sparse.COO(np.array([[1, 0], [0, 0]]))), + ] + + for count, arr in arrs: + area = np.prod(arr.shape) + heatmap = to_spy_heatmap(arr, buckets=1, shading="absolute") + self.assertEqual(len(heatmap), 1) + self.assertAlmostEqual( count / area, heatmap[0][0], places=2) + + +if __name__ == '__main__': + unittest.main()