From 894ed692ce5b9fe6d4eb5061bdbb1cc128aca412 Mon Sep 17 00:00:00 2001 From: Adam Lugowski Date: Wed, 30 Aug 2023 17:50:43 -0700 Subject: [PATCH] Add numpy support --- README.md | 6 +- demo-numpy.ipynb | 181 ++++++++++++++++++++++++++++++++ matspy/__init__.py | 13 ++- matspy/adapters/numpy_driver.py | 18 ++++ matspy/adapters/numpy_impl.py | 39 +++++++ matspy/spy_renderer.py | 10 +- tests/test_numpy.py | 51 +++++++++ 7 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 demo-numpy.ipynb create mode 100644 matspy/adapters/numpy_driver.py create mode 100644 matspy/adapters/numpy_impl.py create mode 100644 tests/test_numpy.py diff --git a/README.md b/README.md index b7021af..cadb40a 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ # MatSpy Sparse matrix spy plot and sparkline renderer. Supports: -* **SciPy** - sparse matrices and arrays like `csr_matrix` and `coo_array` -* **[Python-graphblas](https://github.com/python-graphblas/python-graphblas)** - `gb.Matrix`. [See demo.](demo-python-graphblas.ipynb) +* **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) Features: * Simple `spy()` method, similar to MatLAB's spy. @@ -53,6 +54,7 @@ All methods take the same arguments. Apart from the matrix itself: * `shading`: `binary`, `relative`, `absolute`. * `buckets`: spy plot pixels (longest side). * `dpi`: determine `buckets` relative to figure size. +* `precision`: For numpy arrays, magnitude less than this is considered zero. Like [matplotlib.pyplot.spy()](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.spy.html)'s `precision`. ### Overriding defaults `matspy.params` contains the default values for all arguments. diff --git a/demo-numpy.ipynb b/demo-numpy.ipynb new file mode 100644 index 0000000..d91c762 --- /dev/null +++ b/demo-numpy.ipynb @@ -0,0 +1,181 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2023-08-31T00:41:41.137570Z", + "start_time": "2023-08-31T00:41:41.017838Z" + }, + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2023-08-31T00:41:41.322466Z", + "start_time": "2023-08-31T00:41:41.135934Z" + }, + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "import scipy\n", + "A = scipy.io.mmread(\"doc/matrices/email-Eu-core.mtx.gz\").todense()" + ] + }, + { + "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-31T00:41:41.608106Z", + "start_time": "2023-08-31T00:41:41.519592Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWYAAAFgCAYAAACIf9MLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAB1G0lEQVR4nO2de5xN9f7/X+M2Y1xGg5mhhpBcivhRmihC5PYtOaeUMjlRTRQphCLJJU5FN93OUaejy9GJ4lQI6SYkciunpFCGE82MUW4z6/fH9trNfs98fNbae8/M3ub9fDw8lr3WZ31ua89er/Ve78/7HeM4jgNFURQlYihX2h1QFEVRAtEfZkVRlAhDf5gVRVEiDP1hVhRFiTD0h1lRFCXC0B9mRVGUCEN/mBVFUSIM/WFWFEWJMPSHWVEUJcLQH2YlrOTm5mLw4MFISUlBTEwMRowYgR9++AExMTF46aWXSrt7EcvNN9+Ms88+u7S7oUQIEfPDnJubi4kTJ+LKK69EYmKi9Q/566+/xpVXXomqVasiMTERN910E/73v/8VKpefn48ZM2agQYMGiIuLQ8uWLfHaa68VKnfzzTcjJiam0L+mTZvq+AzcfPPN6NSpU8C+qVOn4qWXXkJGRgZeeeUV3HTTTUHX74Vt27bhwQcfxA8//FDk8fz8fMyZMwetWrVC5cqVUbNmTXTu3BlfffWVsc558+YhJiYGVatWLXSsU6dOuPnmm8PU+9OPZ555Rm/EIVChtDtAfvnlFzz00EOoV68eLrjgAnz44YfGsnv27MFll12GhIQETJ06Fbm5ufjrX/+KzZs3Y+3atahUqZK/7Pjx4zF9+nQMGTIEF154Id5++23ccMMNiImJQf/+/QPqjY2NxYsvvhiwLyEhQcfngRUrVuDiiy/GxIkT/ftMP5bhZNu2bZg0aRI6depUpPL8y1/+gnnz5mHgwIEYNmwYDh8+jA0bNmD//v1F1pebm4vRo0ejSpUqxdzz05NnnnkGtWrV0ptXsDgRwpEjR5y9e/c6juM469atcwA4c+fOLbJsRkaGU7lyZefHH3/071u2bJkDwHnuuef8+/bs2eNUrFjRGTp0qH9ffn6+c+mllzpnnXWWc+LECf/+9PR0p0qVKq77+o9//MN4/J133nEyMzOjdnxuSU9Pdzp27Biwr0GDBk6vXr0C9u3cufOU4w0H8+fPdwA4K1euLHTsjTfecAA4b731luv6xowZ4zRp0sQZMGBAkfPWsWNHJz09PYQeB5Kenu7Ur18/bPWVNuedd16h70YkkZ+f7/z2229FHvv999+dvLy8Eu5RIBFjyoiNjUVKSoqrsv/+97/Ru3dv1KtXz7+va9euOPfcc/Gvf/3Lv+/tt9/G8ePHcccdd/j3xcTEICMjA3v27MHq1asL1Z2Xl4ecnJxTtv/mm29i4MCBeOKJJwodW7JkCfr164dHH300ascXDB9++CFiYmKwc+dO/Oc///GbSk6lllesWIFLL70UVapUQY0aNXDVVVfh66+/Dijz448/4o477kCTJk38Jog///nPAfW+9NJL+POf/wwAuPzyy/1t86nksccew0UXXYS+ffsiPz8fhw8fPuVYvv32Wzz++ON47LHHUKFC6A+V//znP9GmTRtUrlwZiYmJ6N+/P3bv3m09Lz8/H7NmzcJ5552HuLg4JCcn47bbbsOvv/4aUO7ss89G79698eGHH6Jt27aoXLkyWrRo4R//W2+9hRYtWiAuLg5t2rTBhg0bCrX1zTff4E9/+hMSExMRFxeHtm3b4p133gko89JLLyEmJgaffvopRo4cidq1a6NKlSro27dvgJnt7LPPxtatW7Fq1Sr/taDJ6/jx45g0aRIaN26MuLg41KxZEx06dMCyZcs8zqqPuXPnonPnzkhKSkJsbCyaN2+OOXPmFCrHOVqyZIl/jp577jn/9/b111/H/fffjzPPPBPx8fHIycnBwYMHce+996JFixaoWrUqqlevjh49egSYv3Jzc1GlShUMHz68UJt79uxB+fLlMW3aNM/jipgfZrf89NNP2L9/P9q2bVvo2EUXXRTwpduwYQOqVKmCZs2aFSrH4wX57bffUL16dSQkJCAxMRFDhw5Fbm5uoXYGDBiAO+64AyNGjMA//vEP//7PPvsM11xzDS699FJMnjw5ascXDM2aNcMrr7yCWrVqoVWrVnjllVfwyiuvoHbt2kWW/+CDD9C9e3fs378fDz74IEaOHInPPvsM7du3D/jRXbduHT777DP0798fTzzxBG6//XYsX74cnTp1wm+//QYAuOyyy3DXXXcBAMaNG+dvu1mzZsjJycHatWtx4YUXYty4cUhISEDVqlXRsGHDgJtcQUaMGIHLL78cPXv2DHlepkyZgoEDB6Jx48Z47LHHMGLECCxfvhyXXXYZsrKyTnnubbfdhlGjRqF9+/aYPXs2Bg0ahHnz5qF79+44fvx4QNnvvvsON9xwA/r06YNp06bh119/RZ8+fTBv3jzcfffduPHGGzFp0iTs2LED1157LfLz8/3nbt26FRdffDG+/vpr3HfffXj00UdRpUoVXH311ViwYEGhft1555346quvMHHiRGRkZGDRokUYNmyY//isWbNw1llnoWnTpv5rMX78eADAgw8+iEmTJuHyyy/HU089hfHjx6NevXr48ssvg5rfOXPmoH79+hg3bhweffRRpKam4o477sDTTz9dqOz27dtx/fXX44orrsDs2bPRqlUr/7HJkyfjP//5D+69915MnToVlSpVwvfff4+FCxeid+/eeOyxxzBq1Chs3rwZHTt2xM8//wwAqFq1Kvr27Ys33ngDeXl5Ae299tprcBwHAwYM8D6wUtXrBk71qM9jRZkSRo0a5QBwjhw54jiO4/Tq1ctp2LBhoXKHDx92ADj33Xeff999993njBkzxnnjjTec1157zUlPT3cAOO3bt3eOHz9eqI68vDzn+uuvdypUqOC8/fbbzqZNm5wzzjjDufDCC51Dhw5F/fiCpX79+q5MGa1atXKSkpKcAwcO+Pd99dVXTrly5ZyBAwf69xX1uLl69epCc2QyZXz55ZcOAKdmzZpOcnKy88wzzzjz5s1zLrroIicmJsZ57733AsovXrzYqVChgrN161bHcUIzAf3www9O+fLlnSlTpgTs37x5s1OhQoWA/dKU8fHHHzsAnHnz5gWc+/777xfaX79+fQeA89lnn/n3LVmyxAFQyCT23HPPFZqnLl26OC1atPB/rxzH96h/ySWXOI0bN/bvmzt3rgPA6dq1q5Ofn+/ff/fddzvly5d3srKy/PtMpowLLrig0PcjFIr6fnTv3r3Q3wXn6P333w/Yv3LlSgeA07Bhw0J1HTlypJBJY+fOnU5sbKzz0EMP+fdxruV3qWXLlkGbc6JOMf/+++8AfKYBSVxcXECZ33//3VU5AJg2bRqmT5+Oa6+9Fv3798dLL72EKVOm4NNPP8Wbb75ZqI5y5crh5ZdfxhVXXIHrrrsOXbt2RUpKCt57770i3+JH2/iKk71792Ljxo24+eabkZiY6N/fsmVLXHHFFXj33Xf9+ypXruz///Hjx3HgwAGcc845qFGjhiuVxSeCAwcO4O2330ZGRgZuuOEGLF++HDVr1sTDDz/sL3vs2DHcfffduP3229G8efOQx/nWW28hPz8f1157LX755Rf/v5SUFDRu3BgrV640njt//nwkJCTgiiuuCDi3TZs2qFq1aqFzmzdvjrS0NP/ndu3aAQA6d+4cYBLj/u+//x4AcPDgQaxYsQLXXnstDh065G/nwIED6N69O7799lv89NNPAW3deuutiImJ8X++9NJLkZeXhx9//NE6JzVq1MDWrVvx7bffWsu6oeD3Izs7G7/88gs6duyI77//HtnZ2QFlGzRogO7duxdZT3p6ekBdgO9vsFw5309kXl4eDhw4gKpVq6JJkyYB372uXbuibt26mDdvnn/fli1bsGnTJtx4441BjSvqfpg5eUePHi107MiRIwFlKleu7KqcibvvvhvlypXDBx98UOTxihUrYvbs2Th+/Dj279+PKVOmoGbNmu4HUwSRNL7ign/ATZo0KXSsWbNm+OWXX/x24N9//x0TJkxAamoqYmNjUatWLdSuXRtZWVmF/vCKgnPQoEED/48S4HsE7dOnD9auXYsTJ04AAB5//HH88ssvmDRpUshjBHy2asdx0LhxY9SuXTvg39dff230COG52dnZSEpKKnRubm5uoXML/vgCf3jbpKamFrmfdurvvvsOjuPggQceKNQOPWtsbZ1xxhkBdZ6Khx56CFlZWTj33HPRokULjBo1Cps2bbKeZ+LTTz9F165d/e8pateujXHjxgFAkT/MJoo6lp+fj8cffxyNGzcO+O5t2rQpoO5y5cphwIABWLhwod+8Nm/ePMTFxfnffXglYtzl3FKnTh0APtUl2bt3LxITE/0qsk6dOli5ciUcxwm4w/PcunXrnrItvmw6ePBgkccPHDiAq6++GrVq1UJKSgqGDBmCJk2ahKS2Iml8kcCdd96JuXPnYsSIEUhLS0NCQoLfFbCgndQE5yA5ObnQsaSkJBw/ftx/E3j44Ydxxx13ICcnx/+CNDc3F47j4IcffkB8fDySkpJc9z0/Px8xMTF47733UL58+ULHT/VklZ+fj6SkpAAVVhBpuy+q/lPtd05mlOMc3nvvvUY1ec4553iq81Rcdtll2LFjB95++20sXboUL774Ih5//HE8++yzGDx4sPX8guzYsQNdunRB06ZN8dhjjyE1NRWVKlXCu+++i8cff7zQ9+NUQqWoY1OnTsUDDzyAv/zlL5g8eTISExNRrlw5jBgxolDdAwcOxMyZM7Fw4UJcf/31ePXVV9G7d++g3VGj7of5zDPPRO3atfHFF18UOrZ27doAg36rVq3w4osv4uuvvw74sVyzZo3/+Kngo11RL7Byc3PRs2dP/Pzzz1i1ahXq1KmDDh064IorrsCnn34a9CquSBlfcVK/fn0Avpcxkm+++Qa1atXy+w+/+eabSE9PD/ByOXLkSKEXZwVvTAWpW7cuUlJSCj2OA8DPP/+MuLg4VKtWDbt27UJubi5mzJiBGTNmFCrboEEDXHXVVVi4cKHbYaJRo0ZwHAcNGjTAueee6/o8nvvBBx+gffv21iefUGjYsCEA39Nf165dw1av6XoAQGJiIgYNGoRBgwYhNzcXl112GR588EHPP8yLFi3C0aNH8c477wSo+FOZiLzw5ptv4vLLL8ff/va3gP1ZWVmoVatWwL7zzz8frVu3xrx583DWWWdh165dePLJJ4NuO+pMGQDQr18/LF68OMDlaPny5fjvf/8b8Ohw1VVXoWLFinjmmWf8+xzHwbPPPoszzzwTl1xyCQDfH/qhQ4cKtTN58mQ4joMrr7wyYP/Ro0dx1VVXYevWrXj33XfRsmVL1K5dG8uWLUP58uVxxRVXIDMzM2rHV9zUqVMHrVq1wssvvxzwA7tlyxYsXbo0wBuifPnyhZTYk08+WegNOH/Ii/J0uO6667B79+4Al6xffvkFb7/9Njp37oxy5cohKSkJCxYsKPTv8ssvR1xcHBYsWICxY8d6Guc111yD8uXLY9KkSYXG4DgODhw4YDz32muvRV5eXpHePSdOnLB6dLglKSkJnTp1wnPPPVfkU1pRq03dUKVKlSL7KMdctWpVnHPOOUWa5GxQuRec2+zsbMydO9dzXab65XWbP39+kTd5ALjpppuwdOlSzJo1CzVr1kSPHj2CbjuiFPNTTz2FrKwsvyvKokWLsGfPHgC+R1o+FowbNw7z58/H5ZdfjuHDhyM3NxczZ85EixYtMGjQIH99Z511FkaMGIGZM2fi+PHjuPDCC7Fw4UJ8/PHHmDdvnv/CZmZmonXr1rj++uv9S5SXLFmCd999F1deeSWuuuqqgH6++eab+OSTT7Bo0aKAFy716tXD0qVLcemll+Kxxx4rpLyiZXxU+8W5Ym/mzJno0aMH0tLScMstt+D333/Hk08+iYSEBDz44IP+cr1798Yrr7yChIQENG/eHKtXr8YHH3xQyJbfqlUrlC9fHo888giys7MRGxvr928dO3Ys/vWvf6Ffv34YOXIkEhIS8Oyzz+L48eOYOnUqACA+Ph5XX311oX4uXLgQa9euLfKYjUaNGuHhhx/G2LFj8cMPP+Dqq69GtWrVsHPnTixYsAC33nor7r333iLP7dixI2677TZMmzYNGzduRLdu3VCxYkV8++23mD9/PmbPno0//elPnvtUFE8//TQ6dOiAFi1aYMiQIWjYsCH27duH1atXY8+ePadctm6iTZs2mDNnDh5++GGcc845SEpKQufOndG8eXN06tQJbdq0QWJiIr744gu8+eabAe52P/zwAxo0aID09PRTLuvu1q0bKlWqhD59+uC2225Dbm4uXnjhBSQlJRV5k/FK79698dBDD2HQoEG45JJLsHnzZsybN8//lCG54YYbMHr0aCxYsAAZGRmoWLFi8I0H5ctRTNClpah/O3fuDCi7ZcsWp1u3bk58fLxTo0YNZ8CAAYVW2zmOz61t6tSpTv369Z1KlSo55513nvPPf/4zoMyvv/7q3Hjjjc4555zjxMfHO7Gxsc55553nTJ061Tl27FiRfd2wYYNxHNu2bQtwPYq28dWqVcu5+OKLjeM7FW7d5RzHcT744AOnffv2TuXKlZ3q1as7ffr0cbZt21ao74MGDXJq1arlVK1a1enevbvzzTffOPXr1y+08u6FF15wGjZs6JQvX76QS9iOHTucvn37OtWrV3cqV67sdO7c2Vm7dq11POFYMfnvf//b6dChg1OlShWnSpUqTtOmTZ2hQ4c627dvD2inqJV/zz//vNOmTRuncuXKTrVq1ZwWLVo4o0ePdn7++Wd/maLm3HEcB0DAqlDH+eNazJw5M2D/jh07nIEDBzopKSlOxYoVnTPPPNPp3bu38+abb/rL0F1u3bp1AefS5azgfGdmZjq9evVyqlWr5gDwu409/PDDzkUXXeTUqFHDqVy5stO0aVNnypQpAd/DzZs3F3L3NPHOO+84LVu2dOLi4pyzzz7beeSRR5y///3vhf6mTHPEvs+fP7/QsSNHjjj33HOPU6dOHady5cpO+/btndWrVzsdO3Y0usH17NmzkOtiMMQ4jguLvVJm2LZtG8477zwsXrwYvXr1Ku3uKGWQ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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matspy import spy\n", + "\n", + "spy(A)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "ExecuteTime": { + "end_time": "2023-08-22T23:04:45.970063Z", + "start_time": "2023-08-22T23:04:45.607749Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "# Precision\n", + "\n", + "Sometimes we may wish to set near-zero values to zero. The `precision` argument does that. Any value `abs(value) < precision` is treated as zero.\n", + "\n", + "This argument is compatible with [matplotlib.pyplot.spy()](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.spy.html)'s `precision` parameter.\n", + "\n", + "As a simple demonstration, use `precision` to filter random values:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "ExecuteTime": { + "end_time": "2023-08-31T00:41:41.616444Z", + "start_time": "2023-08-31T00:41:41.604573Z" + }, + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
precision = 0precision = 0.2precision = 0.8
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "arr = np.random.random((100, 100))\n", + "\n", + "from IPython.display import display, HTML\n", + "from matspy import to_sparkline\n", + "\n", + "precisions = [0, 0.2, 0.8]\n", + "display(HTML(f''\n", + " f''\n", + " f''\n", + " f''\n", + " f\"\"\n", + " f\"\"\n", + " f\"\"\n", + " f\"
precision = {precisions[0]}precision = {precisions[1]}precision = {precisions[2]}
{to_sparkline(arr, sparkline_size=1.5, precision=precisions[0])}{to_sparkline(arr, sparkline_size=1.5, precision=precisions[1])}{to_sparkline(arr, sparkline_size=1.5, precision=precisions[2])}
\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-08-31T00:41:41.617408Z", + "start_time": "2023-08-31T00:41:41.615424Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [] + } + ], + "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 b59644f..8ef96d7 100644 --- a/matspy/__init__.py +++ b/matspy/__init__.py @@ -54,6 +54,14 @@ class MatSpyParams: buckets: int = None """Pixel count of longest side of spy image. If None then computed from size and DPI.""" + precision: float = None + """ + Applies to dense matrices like numpy arrays. If None or 0, nonzero values are plotted. Else only values with + absolute value > `precision` are plotted. + + Behaves like `matplotlib.pyplot.spy`'s `precision` argument, but for dense arrays only. + """ + spy_aa_tweaks_enabled: bool = None """ Whether to_sparkline() may tweak parameters like bucket count to prevent visible aliasing artifacts. @@ -117,6 +125,9 @@ def _register_bundled(): from .adapters.scipy_driver import SciPyDriver register_driver(SciPyDriver) + from .adapters.numpy_driver import NumPyDriver + register_driver(NumPyDriver) + from .adapters.graphblas_driver import GraphBLASDriver register_driver(GraphBLASDriver) @@ -125,7 +136,7 @@ def _register_bundled(): def _get_driver(mat): - type_str = ".".join((mat.__module__, mat.__class__.__name__)) + type_str = ".".join((type(mat).__module__, type(mat).__name__)) for prefix, driver in _driver_prefixes.items(): if type_str.startswith(prefix): return driver diff --git a/matspy/adapters/numpy_driver.py b/matspy/adapters/numpy_driver.py new file mode 100644 index 0000000..4981796 --- /dev/null +++ b/matspy/adapters/numpy_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 NumPyDriver(Driver): + @staticmethod + def get_supported_type_prefixes() -> Iterable[str]: + return ["numpy."] + + @staticmethod + def adapt_spy(mat: Any) -> MatrixSpyAdapter: + from .numpy_impl import NumPySpy + return NumPySpy(mat) diff --git a/matspy/adapters/numpy_impl.py b/matspy/adapters/numpy_impl.py new file mode 100644 index 0000000..1b7563c --- /dev/null +++ b/matspy/adapters/numpy_impl.py @@ -0,0 +1,39 @@ +# 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 numpy as np +from scipy.sparse import csr_matrix + +from . import describe, MatrixSpyAdapter +from .scipy_impl import SciPySpy + + +class NumPySpy(MatrixSpyAdapter): + def __init__(self, arr): + super().__init__() + if len(arr.shape) != 2: + raise ValueError("Only 2D arrays are supported") + self.arr = arr + + def get_shape(self) -> tuple: + return self.arr.shape + + def describe(self) -> str: + format_name = "array" + + return describe(shape=self.arr.shape, nz_type=self.arr.dtype, + notes=f"{format_name}") + + def get_spy(self, spy_shape: tuple) -> np.array: + precision = self.get_option("precision", None) + + if not precision: + mask = (self.arr != 0) + else: + mask = (self.arr > precision) | (self.arr < -precision) + + if self.arr.dtype == 'object': + mask = mask & (self.arr != np.array([None])) + + return SciPySpy(csr_matrix(mask)).get_spy(spy_shape) diff --git a/matspy/spy_renderer.py b/matspy/spy_renderer.py index 4a4204d..8a2ffe6 100644 --- a/matspy/spy_renderer.py +++ b/matspy/spy_renderer.py @@ -59,12 +59,16 @@ def _rescale(arr, from_range, to_range): # noinspection PyUnusedLocal def get_spy_heatmap(adapter: MatrixSpyAdapter, buckets, shading, shading_absolute_min, - shading_relative_min, shading_relative_max_percentile, **kwargs): + shading_relative_min, shading_relative_max_percentile, precision, **kwargs): # find spy matrix shape mat_shape = adapter.get_shape() + if mat_shape[0] == 0 or mat_shape[1] == 0: + return np.array([[]]) + ratio = buckets / max(mat_shape) spy_shape = tuple(max(1, int(ratio * x)) for x in mat_shape) + adapter.set_option("precision", precision) dense = adapter.get_spy(spy_shape=spy_shape) dense[dense < 0] = 0 @@ -236,6 +240,10 @@ def to_sparkline(mat, retscale=False, scale=None, html_border="1px solid black", repeat = int(repeat) if repeat >= 2 else 1 heatmap = to_spy_heatmap(adapter, **options.to_kwargs()) + if heatmap.size == 0: + # zero-size + return "▫" # a single character that is an empty square + if repeat > 1: heatmap = heatmap.repeat(repeat, axis=0) heatmap = heatmap.repeat(repeat, axis=1) diff --git a/tests/test_numpy.py b/tests/test_numpy.py new file mode 100644 index 0000000..0156199 --- /dev/null +++ b/tests/test_numpy.py @@ -0,0 +1,51 @@ +# 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 + +import numpy as np + +from matspy import spy_to_mpl, to_sparkline, to_spy_heatmap + +np.random.seed(123) + + +class NumPyTests(unittest.TestCase): + def setUp(self): + self.mats = [ + np.array([[]]), + np.random.random((10, 10)), + ] + + def test_no_crash(self): + import matplotlib.pyplot as plt + for mat in self.mats: + fig, ax = spy_to_mpl(mat) + plt.close(fig) + + res = to_sparkline(mat) + self.assertGreater(len(res), 5) + + def test_shape(self): + arr = np.array([]) + with self.assertRaises(ValueError): + spy_to_mpl(arr) + + def test_count(self): + arrs = [ + (1, np.array([[1]])), + (1, np.array([[1, 0], [0, 0]])), + (1, np.array([[1, None], [None, None]])), + (1, np.array([[1, 0], [None, None]])), + ] + + 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(heatmap[0][0], count / area, places=2) + + +if __name__ == '__main__': + unittest.main()