From b41029d9c006c5ff08975f705c9fd349bc37b98e Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 20 Aug 2021 17:09:19 +0300 Subject: [PATCH] refactor(drawing): start using graphviz for visualization --- .github/workflows/continuous-integration.yaml | 2 + docker/Dockerfile | 3 +- examples/QuantizedLinearRegression.ipynb | 28 +- examples/QuantizedLogisticRegression.ipynb | 66 +++-- hdk/common/compilation/artifacts.py | 3 +- hdk/common/debugging/drawing.py | 247 ++++-------------- hdk/common/representation/intermediate.py | 27 ++ hdk/hnumpy/tracing.py | 4 +- poetry.lock | 44 ++-- pyproject.toml | 2 + tests/hnumpy/test_compile.py | 2 +- tests/hnumpy/test_debugging.py | 6 +- 12 files changed, 186 insertions(+), 248 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index f257e30a0..d2931efe7 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -24,6 +24,8 @@ jobs: steps: - name: Install Git run: apt-get install git -y + - name: Install Graphviz + run: apt-get install graphviz* -y - name: Checkout Code uses: actions/checkout@v2 with: diff --git a/docker/Dockerfile b/docker/Dockerfile index 71a27ddfd..b3db5a053 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,7 @@ FROM ghcr.io/zama-ai/zamalang-compiler -RUN apt-get install --no-install-recommends -y python3.8 python3.8-venv python-is-python3 git && \ +RUN apt-get install --no-install-recommends -y \ + python3.8 python3.8-venv python-is-python3 git graphviz* && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry && \ echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index f5e4cf690..a552f080b 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -641,7 +641,7 @@ "id": "f0b08a0f", "metadata": {}, "source": [ - "### Here is the textual representation of the operation graph" + "### Here are some representations of the operation graph" ] }, { @@ -670,6 +670,28 @@ "print(get_printable_graph(homomorphic_model, show_data_types=True))" ] }, + { + "cell_type": "code", + "execution_count": 21, + "id": "785c50ce", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from hdk.common.debugging import draw_graph\n", + "draw_graph(homomorphic_model).show()" + ] + }, { "cell_type": "markdown", "id": "ade14f17", @@ -682,7 +704,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "dd2d03d7", "metadata": {}, "outputs": [], @@ -705,7 +727,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "57050b5d", "metadata": {}, "outputs": [ diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index ccf05f67b..834cac1d2 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -173,22 +173,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "Epoch: 1 | Loss: 0.5758869647979736\n", - "Epoch: 101 | Loss: 0.13611836731433868\n", - "Epoch: 201 | Loss: 0.08021673560142517\n", - "Epoch: 301 | Loss: 0.05636058747768402\n", - "Epoch: 401 | Loss: 0.043306026607751846\n", - "Epoch: 501 | Loss: 0.03511128947138786\n", - "Epoch: 601 | Loss: 0.029501130804419518\n", - "Epoch: 701 | Loss: 0.025424323976039886\n", - "Epoch: 801 | Loss: 0.02233024500310421\n", - "Epoch: 901 | Loss: 0.01990305446088314\n", - "Epoch: 1001 | Loss: 0.0179488193243742\n", - "Epoch: 1101 | Loss: 0.01634199731051922\n", - "Epoch: 1201 | Loss: 0.014997857622802258\n", - "Epoch: 1301 | Loss: 0.013856985606253147\n", - "Epoch: 1401 | Loss: 0.012876608408987522\n", - "Epoch: 1501 | Loss: 0.012025204487144947\n" + "Epoch: 1 | Loss: 0.9475528597831726\n", + "Epoch: 101 | Loss: 0.13412582874298096\n", + "Epoch: 201 | Loss: 0.07946280390024185\n", + "Epoch: 301 | Loss: 0.05598355457186699\n", + "Epoch: 401 | Loss: 0.04308217763900757\n", + "Epoch: 501 | Loss: 0.034963589161634445\n", + "Epoch: 601 | Loss: 0.02939651347696781\n", + "Epoch: 701 | Loss: 0.025346478447318077\n", + "Epoch: 801 | Loss: 0.022270068526268005\n", + "Epoch: 901 | Loss: 0.019855139777064323\n", + "Epoch: 1001 | Loss: 0.01790979877114296\n", + "Epoch: 1101 | Loss: 0.016309652477502823\n", + "Epoch: 1201 | Loss: 0.014970536343753338\n", + "Epoch: 1301 | Loss: 0.013833633624017239\n", + "Epoch: 1401 | Loss: 0.012856445275247097\n", + "Epoch: 1501 | Loss: 0.012007634155452251\n" ] } ], @@ -289,9 +289,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[4.53586054]\n", - " [2.37015319]]\n", - "-14.660321235656738\n" + "[[4.53723335]\n", + " [2.37176466]]\n", + "-14.666179656982422\n" ] } ], @@ -748,7 +748,7 @@ "id": "f0b08a0f", "metadata": {}, "source": [ - "### Here is the textual representation of the operation graph" + "### Here are some representations of the operation graph" ] }, { @@ -783,6 +783,28 @@ "print(get_printable_graph(homomorphic_model, show_data_types=True))" ] }, + { + "cell_type": "code", + "execution_count": 21, + "id": "9d1ff32f", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICqlXEuTnVL+IAk4CeyEAvsL0Kp+K4IC0ya8xMREujXoBluB/cAlC+JUykVscqM+yEop7xEL7AF2AruAg5gEdxQ4DSQ43jqf+XSgA4Kj+YcTzlnOkoc8jjfmBcoAJYDSwJ1ANaA6UBawue7rKOVEc3WcnVLe6giwClgJbAYOkCah3UgrWpGHPFzhCgDBBPM4j6dNdABXgD+Tf66XH5P4GgFNgSbJzynlgfTMTilvcQ34Cfgek+D+usn7A4HiQARQEnOGVhQIA/JAr5m9mLllJnEJcQAseXYJLcu1hDggGjiOOTM8DvwNnM/C/moD9wOPA3Wy+wWVcpm5muyU8mRXgR+BecASTBLKyO1ATcyZVo3k35HccI6kX375hYceegiAggUL8s8//xAcHJz5P7gE7MZcJrVfKt1G5vf2ymKS3hNAffSSp7KSJjulPNJ+YAowCYjK4PWSmMuHLYAHgHLZ30VCQgLFixcnKiqKgQMH8uWXX2b/QxIxlzjXA/9N/snoDDAS6AP0A7Szp3I/TXZKeQwBFgFjgRXJj1O7C3OW9Dimo0hmEoHDmDOvI8AJ4FSq37HARSAJhlwcwpikMawJW0Pj8MbmEmc4UBAohemYEpG8HQlUBYrdYN8JybHPB74D/rnu9XxAN2AoUPkGn6OUc2myU8oj/AD8G9hx3fNlgKeBzsAdGfw7wfTAXIs5u9oF7MUktCzYwAY605kjHCEgqyORbsEkvbuBxsC9mHuD10sEVgMzgNlATKrXAoGumO+c0fdSyrk02SllqWXAa5jelHY2oDnwDNAGkxhSO43ppPIjsA44l8V92c/W8gAFzOdKfmHi0Yn0L9HfJMhrmKR0Nnk/MZl+WlqVgGZAW0wHldDrXj8LfAWMx5x12gUBPYA3MIldKdfQZKeUJU4CzwFzUj1nw1yiHInpYJLa38A3wEJgI2aQeEbK4BgLdyfmUmEpzD2+8Iz/iYhgs2XSeyQqOdZjmM4pe3GcPWbWMSU/8HDyd2lH2sSXhOls8ybmjNQuX/JzQ9CFx5QraLJTyq0E+Bp4nrQdT1oAH2AuDdolYnpgTk7+ff0YugDMfbzGmM4qjTBJzR2SMMlvDeby6RrMPcHr3QJ0x3RMqXLdv58PvA78L9XzNYAJQAPnh6z8miY7pdzmJPAkZoycXVXMpb1GqZ67hklwH2NmP0ktDHOJsy3mEmdG98qssh3TwWYR6e89grm8+SomfrsEYAzm3p19WEUg5tLu66S/hKtUzmiyU8otVmIS3cnkx+HAS8ArOC7zXQG+BD7B3C9L7R7M2VEHzCU/T3cQmJr88/d1r90DjABapXrub2A45qzXrhnwLe47W1W+TJOdUi4lwFvJP/b7bHWBmUCFVO+ZDfwLM1uJXR6gNzCA9PfwvEUC5hLsGMwYvNSaA5+T9vLmXExSv5j8uATmvmZj14apfJ4mO6VcJhHTo3JiqueexvzhD0l+vAsYjOmib5c/+d+9wI3HtHmbTcAoYDGOMYTBmO//BqaHKJhp0Dolvx/Mme//YcYYKpUzc3WJH6VcIQbzx9me6PIBCzCdL0Iwf+w/wcwfaU90QZjB1keA9/GtRAdmyrDvgd9w3KOMB0Zjpjr7Nfm52zFl0j/5cSzQBTN0Qakc0mSnlLPFAo9ihgkA3AosB+wLgJ8AHgRexDH4+z5MB4/P8P3ptO7G9N78P8ywCDBj75pghh8kYM7mxgNvJ7+eADyFmV1GqRzQy5hKOZMAPXF0tCiFWamgevLj34DWmGm7wKwXNxpzn8ofXcJcxkzdMeVhzH06+3JB0zDlk4A5PJ+NXtJU2aWXMZVyqhdx/OGOADbgSHQ/Ybrf2xNdHcyq4P6a6MDcp5uB6ZhiP6P9CWiIY9hFL8wlTBumk093zPRoSmWDJjulnGUS8GnydkFML8Tbkx9PxVzavJz8uCdmMHYldwbowZ7AdEiJTH68CzPn5sHkx92B95K3YzAzs1w/BlGpG9Bkp5QzHMD0ngTTw3AuZjYQMPfunsb0zrRhpgObiqNHpjLuwHRSaZr8+DjmXqY9qb2MmU4MzOwzT2LKVKks0GSnVG4lYJatsc8A8iFmjTmAnzHd6BMwie5LTDd7Xcg0Y0UwZdYy+fGx5G37ZNefYAalg5kE+2O3Rqe8mCY7pXLrPRxjwprjOPs4hlnGJi758fuYAeLqxkIx82Y2SX68B3MZUzDDM77G0Xnl35hLnkrdhCY7pXLjNOZMDqAwpudgAGb8WAcckz3/CzM9mMqacMyYPPvMMUtxnMVVAP6TvB2HmXJNqZvQZKdUbryF4/Llv4HSydsjcZztNcHRuUJlXUHMvU/7XKAjML1XwUyj1jB5ezGwyq2RKS+k4+yUyqlDmDXj4oCywJ+YS3AHMasZxGLuQW0HbrMmRJ8wCzODCpj7desx9zxXYyaLBjMjiw5HUJnTcXZK5dgEHPfj3sSxesEwHDOjfIImutzqjBloDqa35tzk7aY4OrKswxxUKJUJTXZK5UQiZvkZMHNY2s88NmHWcwMzLVYPN8flqz7BsYL5qzhWkHgu1XumuzMg5W002SmVEz/jWI6nO2ZsHZj5HO0+xCUtrGbNmthstiz/vPPOO+TLly/d8x9/nL7f/vHjxzP8jIULF6Z532uvvZbuPX/++afzv6xdFcx9OjCXiZclb7fAceb8fzjOqJW6jiY7pXJibqrtnsm/L6Z6vhJmajBX7X7uXEQk5ad/f7NEwNKlS9M836lTJwCio6PZvt1c52vbti0iwosvvpjuc0uXLo2IMHPmTABefvllRIR27dqled8777yDiNC0aVMmTZqEiFC5cmXXfWEwc2jaTUr+HYA52AAzFm+Fa0NQ3kuTnVI5sT75dzkcc18uwKw2Dma+Sx047lw1MAvfghmWYF/gtU2q92xwa0TKiwTd/C1KqTTOYqYHA0f3dzDL1th1ct3ud+zYkeX3zpo1y3WBWKETsAUzjnEDpoNKLcy4vGs41sRT6jp6ZqdUdv2KY6Xt+qmet3d9L4djvJ1yrsaptu1n18GYzkAAm9H5MlWGNNkplV2HUm3XTP59DscM/fe6NRr/UguzBiA4Bu2D4//DZeCMOwNS3kKTnVLZdS7VdrHk36dSPXeHG2PxN8E4lk1KXeZFU21HoVQ6muyUyq7Uyc6+4OjZVM/d6sZY/JG9fDMr89T/f5RKpslOqey6mGq7UPLvC6meK+y2SFwmMDAQgMTEG98AS0xMTHmv29gPMFKfwaUu8/NujEV5DU12SmVXnlTbV5N/50313BW8Xr58ZvblS5cu3fB9Fy5coECBAu4IycE+8Xa+DJ6DtP8vlEqmyU6p7Lol1fa5DJ5LfXnNS0VGRgKwe/fuTN8TGxvLgQMHqFixorvCMv5J/p3ZpcvU/y+USqbJTqnsKpJq2/5HNvUf3pNujMXJgoKC+PPPP6lQoQKVK1dm48aN7N+/P8P3zpkzh6JFi1KtWrUMX3cZe/lqslPZoMlOqewqmWrbngdK40iCG90bjquMHj2agIAAWrZsyYIFC4iKiiIxMZG///6bL7/8ksGDB/Ppp58SEODGPyMHcQwtqJ7qefv/hyDS9sxUKpmuZ6dUdu3DzH0JZr7GMcnbrTELiQZhOk/kd30o06ZNo3fv3umev3z5csp9NzD34K5cydrNxL1796bMc7lt2zZGjx7NunXrOHnyJCJCsWLFuOeee3juuedo2LDhTT7NyabhmBD6a6Bb8nYJzKrxtYBt7g1JeYW5muyUyi7BjK87C9QGfkt+/gNgePL2POBx94fm8zrimGz7MGbR3AOA/bbhM8AX7g9LeTxdvFWpbLMBDZK3/8DRIeUxHJM/T3Z3UH7gLGYCaIC7MIkOYHmq9zRAqQxpslMqJx5J/h2PYxHXSMzq2QC/AEfcHJOvm4pjvboBqZ63L9oaBDzg1oiUF9Fkp1ROdMXMtA9pV8jul/w7CRjp1oh82yXAvtZsPkz5g7l/au8Q1Apz706pDGiyUyonCuJYR20bZrZ9MPeUqiZvf41Zjkbl3igcvTCfA+zj2MfjWIGil3tDUt5FO6golVP/xXHZrAWwLHl7MaZnJph7SGvRlSNzYzdQB4jBnLntw/R0PY65dHwNKIW5bBxsTYjK42kHFaVyrAVwf/L2f5N/AB7FkQQ3Aq+5OS5fcgVzthyT/HgUjiEdb2ASHcC/0USnbkjP7JTKjY2Y1coFs6baZswf3WOYMV/nMDcLFmNW1VbZ0wvHPdE2wEJMj9ffMWd7CZhhB7vRZKduRM/slMqVBkC75O0dwFvJ22WAGZg/zElABxwra6us+TeORFcG+ApTnjGYweQJya+9gyY6dVOa7JTKrTE45mN8F1idvP0IjkHmVzBnJrvcG5rX+hR4O3k7DzAHRxkPx1GOLTEHEkrdhCY7pXIrAhiXvJ2Emc7K3nNwFPBU8nYU0BzY5NbovM97wIvJ28GYGVPsg8UXAp8nbxfHnPnZUOqmNNkp5QwdgB7J24cxY76iMX+IJwCdk187gxl4PsvdAXqBBMx0X69i7oEGYC4F2wfwb8FcvhRMuU5GJ31WWabJTilnGQfck7z9G9AJ8wfc/ke7T/JrscCTmN6ECSiAv4EHcZwhh2MuXdoPEg5ierna57J+M/mxUlmkyU4pZ8mDucxWIfnxEsxMH7GYy3FTgM8wrS4J8wf7XsxExv5sIVADWJn8+BbMdGv2ibT/xAzxsF8a7ge87sb4lE/QZKeUMxUDfsbcTwJzv6klZrorgKGYS5gFkx9vxqyc8AX+d5Z3GjO04DEci6/WwVyubJT8+DegCXA0+fEjwJfuC1H5Dk12SjlbBcwA81LJj1diOqacSn7cAbNaQpPkx5cw6+JVxyRKXxcP/AezJqB9aIENGIIZnlEu+bmlwH3AP8mP22KWTtLZaFQOaLJTyhWqARtwLPL6G+ZS3S/Jj28DVmB6a+ZJfu5P4GHMVGO+2GMzHrNyQVXM/JYXk5+/A7NMz3+AEEwHlA8w5RCd/J4emEQXjlI5oslOKVe5HVgD1E1+/A/mMtxbmHt2gZieh3tJO1ZsMaar/QOYhOjtcxxdwVx6rIjppLM/+fl8mGEGuzBncGDOfltgxtIlYs74XsGsUK5ndCoXdLowpVwtFniBtPea7sXM2F8t1XMrgRHAr9f9+0jMWL2emHuC3uI3zPCAmTjuWYI5e+uB6aBjv9QrwP9hxtfZO6IUxMya0t4dwSofN1eTnVLu8h3mzOZC8uMgzLiyUZizHLvlmCmwVl3370Mw3fPbYWZj8cQxZr8Di4D5mPuSqYUBfYGXMJdx7Q5gymFZqudqYYYe3OGySJV/0WSnlFsdBJ7GXJ60K4s5y3kSc2nTbhNmQPocHOPL7AIxE1A/CDQG6mHN/axTwDrM5drFmAH11yuHSfJPkXZx1X8w9+bG4liBPBgYhhmDGOqSiJV/0mSnlCXmAs9iut/blcPcq+pL2qR3CfgW03NxM+Z+3/VCMN3262A6gFQFqgCFnRSvAH8BezArDOzCdMDJbIxgPsyg7z6YnqipewdEYab8Gk3ay5uNMIPKU1/aVco5NNkpZZnzmLXuJmF6KtpVwQxF6IZj7Ta7k8D3mIHYK3GcEWWmGFASM39ncaA0kDf5JyTV7ytAHKb3YzwmCZ8BTiRvHyX92WVG+2qDGSLQAnPZMrX9mGT2FY6emGDu272NmVNU57lUrqHJTinLHcb00Pw/0g4sLwB0BwaQ8dnOVWAbZmzauuTf510aaVolMWdj9yb/rkX6/t0JmMub4zD35FL/tSmGOZMdgA4pUK6myU4pj/E/TGeVOaQ/Y6sOPJH8UyWTf5+ESZx7cFxuPIQ5G/wbx2rf2VEYc+ZVCjNmsCpwJyb53pLJv4nH3JOch+mUc+6610thBpAPxpxZKuV6muyU8jhnMPNoTsDcJ7teFcx4vaaYzikFM3hPRs5jEt81HJct7b/zYTqH2H8XwZy5XX8pMjOHMOv4rQR+xNyXS80GNMP0umyHjplT7qbJTimPlYiZTHom5lLg5QzeE4i5fNgYqIk546qKa3synsMMMdiFGUu3CjiWyXvLY85Ge2HOCJWyhiY7pbxCDPAT5tLgjzjG6mUkCDNbSWWgDI7LkKUxY/PyYO6RhSVvh+LomHIJk2TtZ4F/J/8cxyS0ncnP30hFzIoFT2AmuVbKeprslPI6iZh5NNdjJpxeTvrLhu5k76jSAjPFWbkbv10pC2iyU8rrJQL7MGddqX/+Sn7NWfJgzharYy6X1kjeLunEfSjlGprslPJZiZgxcscxlx6PYS5/RmN6e17FdFaJwQxzCAQKYYYPFMKMyyuBufxZCtNpRSnvNFf7RCnlqwJx3K9Tys/pEj9KKaV8niY7pZRSPi8IMyWtUkop5as2/j+OlP2jmx65gwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from hdk.common.debugging import draw_graph\n", + "draw_graph(homomorphic_model).show()" + ] + }, { "cell_type": "markdown", "id": "ade14f17", @@ -795,7 +817,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "dd2d03d7", "metadata": {}, "outputs": [], @@ -818,7 +840,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "57050b5d", "metadata": {}, "outputs": [ diff --git a/hdk/common/compilation/artifacts.py b/hdk/common/compilation/artifacts.py index 63f9f665d..32bb585ae 100644 --- a/hdk/common/compilation/artifacts.py +++ b/hdk/common/compilation/artifacts.py @@ -72,9 +72,8 @@ class CompilationArtifacts: draw_graph( self.operation_graph, + show=False, save_to=output_directory.joinpath("graph.png"), - block_until_user_closes_graph=False, - draw_edge_numbers=True, ) if self.bounds is not None: diff --git a/hdk/common/debugging/drawing.py b/hdk/common/debugging/drawing.py index c41c648c2..0f854f27d 100644 --- a/hdk/common/debugging/drawing.py +++ b/hdk/common/debugging/drawing.py @@ -1,10 +1,12 @@ """functions to draw the different graphs we can generate in the package, eg to debug.""" +import tempfile from pathlib import Path -from typing import Dict, List, Optional +from typing import Optional import matplotlib.pyplot as plt import networkx as nx +from PIL import Image from ..operator_graph import OPGraph from ..representation import intermediate as ir @@ -22,222 +24,71 @@ IR_NODE_COLOR_MAPPING = { } -def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float = 1.0) -> Dict: - """Returns positions for graphs, to make them easy to read. - - Returns a pos to be used later with eg nx.draw_networkx_nodes, so that nodes - are ordered by depth from input along the x axis and have a uniform - distribution along the y axis - - Args: - graph (nx.Graph): The graph that we want to draw - x_delta (float): Parameter used to set the increment in x - y_delta (float): Parameter used to set the increment in y - - Returns: - pos (Dict): the argument to use with eg nx.draw_networkx_nodes - - """ - nodes_depth = {node: 0 for node in graph.nodes()} - input_nodes = [node for node in graph.nodes() if len(list(graph.predecessors(node))) == 0] - - # Init a layout so that unreachable nodes have a pos, avoids potential crashes wiht networkx - # use a cheap layout - pos = nx.random_layout(graph) - - curr_x = 0.0 - curr_y = -(len(input_nodes) - 1) / 2 * y_delta - - for in_node in input_nodes: - pos[in_node] = (curr_x, curr_y) - curr_y += y_delta - - curr_x += x_delta - - curr_nodes = input_nodes - - current_depth = 0 - while len(curr_nodes) > 0: - current_depth += 1 - next_nodes_set = set() - for node in curr_nodes: - next_nodes_set.update(graph.successors(node)) - - curr_nodes = list(next_nodes_set) - for node in curr_nodes: - nodes_depth[node] = current_depth - - nodes_by_depth: Dict[int, List[int]] = {} - for node, depth in nodes_depth.items(): - nodes_for_depth = nodes_by_depth.get(depth, []) - nodes_for_depth.append(node) - nodes_by_depth[depth] = nodes_for_depth - - depths = sorted(nodes_by_depth.keys()) - - for depth in depths: - nodes_at_depth = nodes_by_depth[depth] - - curr_y = -(len(nodes_at_depth) - 1) / 2 * y_delta - for node in nodes_at_depth: - pos[node] = (curr_x, curr_y) - curr_y += y_delta - - curr_x += x_delta - - return pos - - -def adjust_limits(): - """Increases the limits of x and y axis of the current pyplot figure by 20%. - - Returns: - None - """ - - x_lim = plt.xlim() - x_distance = x_lim[1] - x_lim[0] - plt.xlim([x_lim[0] - x_distance / 10, x_lim[1] + x_distance / 10]) - - y_lim = plt.ylim() - y_distance = y_lim[1] - y_lim[0] - plt.ylim([y_lim[0] - y_distance / 10, y_lim[1] + y_distance / 10]) - - def draw_graph( opgraph: OPGraph, - block_until_user_closes_graph: bool = True, - draw_edge_numbers: bool = True, + show: bool = False, + vertical: bool = True, save_to: Optional[Path] = None, -) -> None: - """Draw a graph. +) -> Image.Image: + """Draws operation graphs and optionally saves/shows the drawing. Args: - opgraph (OPGraph): The graph that we want to draw - block_until_user_closes_graph (bool): if True, will wait the user to - close the figure before continuing; False is useful for the CI tests - draw_edge_numbers (bool): if True, add the edge number on the arrow - linking nodes, eg to differentiate the x and y in a Sub coding - (x - y). This option is not that useful for commutative ops, and - may make the picture a bit too dense, so could be deactivated - save_to (Optional[Path]): if specified, the drawn graph will be saved - to this path + opgraph (OPGraph): the graph to be drawn and optionally saved/shown + show (bool): if set to True, the drawing will be shown using matplotlib + vertical (bool): if set to True, the orientation will be vertical + save_to (Optional[Path]): if specified, the drawn graph will be saved to this path Returns: - None + Pillow Image of the drawn graph. + This is useful because you can use the drawing however you like. + (check https://pillow.readthedocs.io/en/stable/reference/Image.html for further information) """ - assert isinstance(opgraph, OPGraph) - set_of_nodes_which_are_outputs = set(opgraph.output_nodes.values()) - graph = opgraph.graph - # Positions of the node - pos = human_readable_layout(graph) - - # Colors and labels - def get_color(node): + def get_color(node, output_nodes): value_to_return = IR_NODE_COLOR_MAPPING[type(node)] - if node in set_of_nodes_which_are_outputs: + if node in output_nodes: value_to_return = IR_NODE_COLOR_MAPPING["output"] elif isinstance(node, ir.ArbitraryFunction): value_to_return = IR_NODE_COLOR_MAPPING.get(node.op_name, value_to_return) return value_to_return - color_map = [get_color(node) for node in graph.nodes()] + graph = opgraph.graph + output_nodes = set(opgraph.output_nodes.values()) - # For most types, we just pick the operation as the label, but for Input, - # we take the name of the variable, ie the argument name of the function - # to compile - def get_proper_name(node): - if isinstance(node, ir.Input): - return node.input_name - if isinstance(node, ir.Constant): - return str(node.constant_data) - if isinstance(node, ir.ArbitraryFunction): - return node.op_name - return node.__class__.__name__ + attributes = { + node: { + "label": node.label(), + "color": get_color(node, output_nodes), + "penwidth": 2, # double thickness for circles + "peripheries": 2 if node in output_nodes else 1, # double circle for output nodes + } + for node in graph.nodes + } + nx.set_node_attributes(graph, attributes) - label_dict = {node: get_proper_name(node) for node in graph.nodes()} + for edge in graph.edges(keys=True): + idx = graph.edges[edge]["input_idx"] + graph.edges[edge]["label"] = f" {idx} " # spaces are there intentionally for a better look - # Draw nodes - nx.draw_networkx_nodes( - graph, - pos, - node_color=color_map, - node_size=1000, - alpha=1, - ) + agraph = nx.nx_agraph.to_agraph(graph) + agraph.graph_attr["rankdir"] = "TB" if vertical else "LR" + agraph.layout("dot") - # Draw labels - nx.draw_networkx_labels(graph, pos, labels=label_dict) + if save_to is None: + with tempfile.NamedTemporaryFile(suffix=".png") as tmp: + agraph.draw(tmp.name) + img = Image.open(tmp.name) + else: + agraph.draw(save_to) + img = Image.open(save_to) - current_axes = plt.gca() + if show: # pragma: no cover + # We can't have coverage in this branch as `plt.show()` blocks and waits for user action. + plt.close("all") + plt.figure() + plt.imshow(img) + plt.axis("off") + plt.show() - # And draw edges in a way which works when we have two "equivalent edges", - # ie from the same node A to the same node B, like to represent y = x + x - already_done = set() - - for e in graph.edges: - - # If we already drew the different edges from e[0] to e[1], continue - if (e[0], e[1]) in already_done: - continue - - already_done.add((e[0], e[1])) - - edges = graph.get_edge_data(e[0], e[1]) - - # Draw the different edges from e[0] to e[1], continue - for which, edge in enumerate(edges.values()): - edge_index = edge["input_idx"] - - # Draw the edge - current_axes.annotate( - "", - xy=pos[e[0]], - xycoords="data", - xytext=pos[e[1]], - textcoords="data", - arrowprops=dict( - arrowstyle="<-", - color="0.5", - shrinkA=5, - shrinkB=5, - patchA=None, - patchB=None, - connectionstyle="arc3,rad=rrr".replace("rrr", str(0.3 * which)), - ), - ) - - if draw_edge_numbers: - # Print the number of the node on the edge. This is a bit artisanal, - # since it seems not possible to add the text directly on the - # previously drawn arrow. So, more or less, we try to put a text at - # a position which is close to pos[e[1]] and which varies a bit with - # 'which' - a, b = pos[e[0]] - c, d = pos[e[1]] - const_0 = 1 - const_1 = 2 - - current_axes.annotate( - str(edge_index), - xycoords="data", - xy=( - (const_0 * a + const_1 * c) / (const_0 + const_1), - (const_0 * b + const_1 * d + 0.1 * which) / (const_0 + const_1), - ), - textcoords="data", - ) - - plt.axis("off") - - adjust_limits() - - # save the figure if requested - if save_to is not None: - plt.savefig(save_to) - - # block_until_user_closes_graph is used as True for real users and False - # for CI - plt.show(block=block_until_user_closes_graph) + return img diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 79a6b9690..1eba9aecf 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -106,6 +106,15 @@ class IntermediateNode(ABC): """ return cls.n_in() > 1 + @abstractmethod + def label(self) -> str: + """Function to get the label of the node. + + Returns: + str: the label of the node + + """ + class Add(IntermediateNode): """Addition between two values.""" @@ -118,6 +127,9 @@ class Add(IntermediateNode): def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] + inputs[1] + def label(self) -> str: + return "+" + class Sub(IntermediateNode): """Subtraction between two values.""" @@ -130,6 +142,9 @@ class Sub(IntermediateNode): def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] - inputs[1] + def label(self) -> str: + return "-" + class Mul(IntermediateNode): """Multiplication between two values.""" @@ -142,6 +157,9 @@ class Mul(IntermediateNode): def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] * inputs[1] + def label(self) -> str: + return "*" + class Input(IntermediateNode): """Node representing an input of the program.""" @@ -173,6 +191,9 @@ class Input(IntermediateNode): and super().is_equivalent_to(other) ) + def label(self) -> str: + return self.input_name + class Constant(IntermediateNode): """Node representing a constant of the program.""" @@ -213,6 +234,9 @@ class Constant(IntermediateNode): """ return self._constant_data + def label(self) -> str: + return str(self.constant_data) + class ArbitraryFunction(IntermediateNode): """Node representing a univariate arbitrary function, e.g. sin(x).""" @@ -257,3 +281,6 @@ class ArbitraryFunction(IntermediateNode): and self.op_name == other.op_name and super().is_equivalent_to(other) ) + + def label(self) -> str: + return self.op_name diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 6b4e40e18..eff5aea12 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -129,7 +129,7 @@ class NPTracer(BaseTracer): arbitrary_func=numpy.rint, output_dtype=common_output_dtypes[0], op_kwargs=deepcopy(kwargs), - op_name="numpy.rint", + op_name="np.rint", ) output_tracer = self.__class__( input_tracers, traced_computation=traced_computation, output_index=0 @@ -151,7 +151,7 @@ class NPTracer(BaseTracer): arbitrary_func=numpy.sin, output_dtype=common_output_dtypes[0], op_kwargs=deepcopy(kwargs), - op_name="numpy.sin", + op_name="np.sin", ) output_tracer = self.__class__( input_tracers, traced_computation=traced_computation, output_index=0 diff --git a/poetry.lock b/poetry.lock index 1512e87b8..681ef239b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -229,7 +229,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "diff-cover" -version = "6.3.1" +version = "6.3.3" description = "Run coverage and linting reports on diffs" category = "dev" optional = false @@ -511,13 +511,14 @@ qtconsole = "*" [[package]] name = "jupyter-client" -version = "6.2.0" +version = "7.0.1" description = "Jupyter protocol implementation and client libraries" category = "dev" optional = false python-versions = ">=3.6.1" [package.dependencies] +entrypoints = "*" jupyter-core = ">=4.6.0" nest-asyncio = ">=1.5" python-dateutil = ">=2.1" @@ -526,8 +527,8 @@ tornado = ">=4.1" traitlets = "*" [package.extras] -doc = ["sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -test = ["async-generator", "ipykernel", "ipython", "mock", "pytest-asyncio", "pytest-timeout", "pytest", "mypy", "pre-commit", "jedi (<0.18)"] +doc = ["myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] +test = ["codecov", "coverage", "ipykernel", "ipython", "mock", "mypy", "pre-commit", "pytest", "pytest-asyncio", "pytest-cov", "pytest-timeout", "jedi (<0.18)"] [[package]] name = "jupyter-console" @@ -974,11 +975,11 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.19" +version = "3.0.20" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.6.2" [package.dependencies] wcwidth = "*" @@ -1068,6 +1069,14 @@ category = "dev" optional = false python-versions = ">=3.5" +[[package]] +name = "pygraphviz" +version = "1.7" +description = "Python interface to Graphviz" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "pylint" version = "2.9.6" @@ -1413,7 +1422,7 @@ test = ["pytest"] [[package]] name = "terminado" -version = "0.11.0" +version = "0.11.1" description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." category = "dev" optional = false @@ -1555,7 +1564,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "65489a7f8c03f8825d0948ffec2ef5809e53d82e5b9eb3c77d5fa512c175d0fd" +content-hash = "382f9225cb89e407123521c4a9ec5aedc021701f7af8ef79e2959ca5996a6be1" [metadata.files] alabaster = [ @@ -1756,8 +1765,8 @@ defusedxml = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] diff-cover = [ - {file = "diff_cover-6.3.1-py3-none-any.whl", hash = "sha256:2578fb51c4a5ce162d9ba7f5dcc28132b55539c889e9b648f9df77d4fdcf8fb4"}, - {file = "diff_cover-6.3.1.tar.gz", hash = "sha256:21baf9d6f40ef352df4adf19b5bb4d47249c540a648fecda4647a41ff558d47c"}, + {file = "diff_cover-6.3.3-py3-none-any.whl", hash = "sha256:4aaffc7051dd6b0e4e39170d2a69f412a21bbbf8497c85654a8d0c1fd44be534"}, + {file = "diff_cover-6.3.3.tar.gz", hash = "sha256:487b9babf6d1a7d73b9f72c2ee4cbed2840bf2f0e203e184b9ef632532115665"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, @@ -1837,8 +1846,8 @@ jupyter = [ {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, ] jupyter-client = [ - {file = "jupyter_client-6.2.0-py3-none-any.whl", hash = "sha256:9715152067e3f7ea3b56f341c9a0f9715c8c7cc316ee0eb13c3c84f5ca0065f5"}, - {file = "jupyter_client-6.2.0.tar.gz", hash = "sha256:e2ab61d79fbf8b56734a4c2499f19830fbd7f6fefb3e87868ef0545cb3c17eb9"}, + {file = "jupyter_client-7.0.1-py3-none-any.whl", hash = "sha256:07b9566979546004c089afe7c9bf9e96224ec5f8421fe0ae460759fa593c6b1d"}, + {file = "jupyter_client-7.0.1.tar.gz", hash = "sha256:48822a93d9d75daa5fde235c35cf7a92fc979384735962501d4eb60b197fb43a"}, ] jupyter-console = [ {file = "jupyter_console-6.4.0-py3-none-any.whl", hash = "sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27"}, @@ -2182,8 +2191,8 @@ prometheus-client = [ {file = "prometheus_client-0.11.0.tar.gz", hash = "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, - {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, + {file = "prompt_toolkit-3.0.20-py3-none-any.whl", hash = "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c"}, + {file = "prompt_toolkit-3.0.20.tar.gz", hash = "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"}, ] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, @@ -2240,6 +2249,9 @@ pygments = [ {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] +pygraphviz = [ + {file = "pygraphviz-1.7.zip", hash = "sha256:a7bec6609f37cf1e64898c59f075afd659106cf9356c5f387cecaa2e0cdb2304"}, +] pylint = [ {file = "pylint-2.9.6-py3-none-any.whl", hash = "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594"}, {file = "pylint-2.9.6.tar.gz", hash = "sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e"}, @@ -2472,8 +2484,8 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] terminado = [ - {file = "terminado-0.11.0-py3-none-any.whl", hash = "sha256:221eef83e6a504894842f7dccfa971ca2e98ec22a8a9118577e5257527674b42"}, - {file = "terminado-0.11.0.tar.gz", hash = "sha256:1e01183885f64c1bba3cf89a5a995ad4acfed4e5f00aebcce1bf7f089b0825a1"}, + {file = "terminado-0.11.1-py3-none-any.whl", hash = "sha256:9e0457334863be3e6060c487ad60e0995fa1df54f109c67b24ff49a4f2f34df5"}, + {file = "terminado-0.11.1.tar.gz", hash = "sha256:962b402edbb480718054dc37027bada293972ecadfb587b89f01e2b8660a2132"}, ] testpath = [ {file = "testpath-0.5.0-py3-none-any.whl", hash = "sha256:8044f9a0bab6567fc644a3593164e872543bb44225b0e24846e2c89237937589"}, diff --git a/pyproject.toml b/pyproject.toml index 5c7eb534c..5d89778af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,8 @@ python = ">=3.7,<3.10" networkx = "^2.6.1" matplotlib = "^3.4.2" numpy = "^1.21.1" +pygraphviz = "^1.7" +Pillow = "^8.3.1" [tool.poetry.dev-dependencies] isort = "^5.9.2" diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 939d3f42c..353bd7ec7 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -61,7 +61,7 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n # TODO: For the moment, we don't have really checks, but some printfs. Later, # when we have the converter, we can check the MLIR - draw_graph(op_graph, block_until_user_closes_graph=False) + draw_graph(op_graph, show=False) str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) print(f"\n{str_of_the_graph}\n") diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 78aad1c31..65679d228 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -137,7 +137,7 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): x, y = x_y graph = tracing.trace_numpy_function(lambda_f, {"x": x, "y": y}) - draw_graph(graph, block_until_user_closes_graph=False) + draw_graph(graph, show=False) str_of_the_graph = get_printable_graph(graph) @@ -167,7 +167,7 @@ def test_hnumpy_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph "Test hnumpy get_printable_graph and draw_graph on graphs with direct table lookup" graph = tracing.trace_numpy_function(lambda_f, params) - draw_graph(graph, block_until_user_closes_graph=False) + draw_graph(graph, show=False) str_of_the_graph = get_printable_graph(graph) @@ -257,7 +257,7 @@ def test_hnumpy_print_with_show_data_types_with_direct_tlu(lambda_f, params, ref """Test hnumpy get_printable_graph with show_data_types on graphs with direct table lookup""" graph = tracing.trace_numpy_function(lambda_f, params) - draw_graph(graph, block_until_user_closes_graph=False) + draw_graph(graph, show=False) str_of_the_graph = get_printable_graph(graph, show_data_types=True)