{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "*This post was entirely written using the IPython notebook. Its content is BSD-licensed. You can see a static view or download this notebook with the help of nbviewer at [20170731_CythonAndComplexNumbers.ipynb](http://nbviewer.ipython.org/urls/raw.github.com/flothesof/posts/master/20170731_CythonAndComplexNumbers.ipynb).*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Last week, I had the pleasure to dive deep into the Cython world in order to solve a physics problem involving complex numbers.\n", "This post describes some of the things I've learnt concerning Cython, complex numbers and parallelization." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# What problem am I trying to solve? " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The problem I was trying to solve was the following: given a vector of complex numbers ($w_i$, \"weights\"), I wanted to compute its inverse Fourier transform given a certain angular frequency, $\\omega$.\n", "\n", "So what I want to compute is this: \n", "\n", "$$\n", "\\mathcal{Re}(\\sum_{i=1}^n w_i \\exp(i \\omega t))\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Beware that $t$ is actually a vector." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Reference implementation with NumPy " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, let's see our reference implementation. We make use of NumPy broadcasting to effectively perform the summation part in a single operation over all time steps." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import numpy as np" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def synthesis(weights, omega, time_vector):\n", " \"\"\"Sums weighted complex exponentials.\"\"\"\n", " return (weights[:, np.newaxis] * np.exp(1j * omega * time_vector[np.newaxis, :])).sum(axis=0).real" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's apply this on some sample data." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "weights = (np.random.randn(1000) + 1j * np.random.randn(1000)) * np.exp(-np.arange(1000))\n", "omega = 2 * np.pi * 1e-3\n", "time_vector = np.arange(2000, dtype=np.float)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "ref_result = synthesis(weights, omega, time_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's inspect the shape and the dtype of the result :" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(2000,)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ref_result.shape" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dtype('float64')" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ref_result.dtype" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is the expected shape since time step vector is of size 2000. Also, our output is floats, meaning the dtype is as expected. Let's plot the result to have some fun." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "plt.style.use('bmh')\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXkAAAD3CAYAAAD4ziQhAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsvXl4W9d95/052AgQJCEuIKl9sSTTsmx5kZfEsWNXdtZJ\nnahZ2yydRzOdvKO0fWfaaZzJM21n+nbSaWc6mT5tJ+2kSzptk6ZJmqSJsziO7Ti2vGmxrMVaLWgh\nQYIgiR3Edt4/gAtBIEBs9+Iegfg+jx4RvBe4n/O9uD+e+zu/e46QUtJVV1111VVnymI2QFddddVV\nV8apG+S76qqrrjpY3SDfVVddddXB6gb5rrrqqqsOVjfId9VVV111sLpBvquuuuqqg6VLkBdCvEMI\ncUoIcVYI8ViF7R4hxD8LIV4VQhwXQvxLPY7bVVddddXV8hKt1skLIazAaeAR4DLwMvARKeWJkn3+\nI+CRUn5aCOEFTgHjUspUSwfvqquuuupqWdl0+Iy7gbNSyvMAQoivAI8CJ0r2kUC/EEIAfcAckCn/\noKefflr29PTogNRVV111tXIUj8dn9+zZ4620TY8gvxa4VPL6MnBP2T5/DHwbmAT6gQ9JKXPlH9TT\n08PExERTED6fj40bNzb1XiPV5WpMqnKBumxdrsbUiVyHDh3yVdumR5CvR28HjgA/A9wAPCGEeFZK\nGS7daWZmhn379mGz2chms+zdu5f9+/fj9/txu91YrVbC4TBer5e5uTmklHi9Xqanp0mlUgSDQaLR\nKGNjYwQCAYQQDA0NEQgEGBgYIJvNEovFGB8fx+/3Y7fb8Xg8zM7O4vF4SKVSJBKJ4naHw0F/fz/B\nYJDBwUESiQTJZLK43el04nK5mJ+fZ3h4mEgkQiqVKm53uVxkMhl8Ph8jIyOEQiHS6XRxe6029fX1\nARjSpkgkQjgcbqpNDoeDUChkSJvS6TSzs7NtP0/1tCkSiRCPx9t6nuppUy6XY2pqqq3nqZ42LS4u\n4vP52n6earUpEomQTCaViRFam6SUTE5ONtWm5aRHTv5NwG9LKd9eeP0ZACnl50r2+S7we1LKZwuv\nfww8JqV8qfSzDhw4IJvtycdiMdxud3ONMFBdrsakKheoy9blakydyHXo0KGDe/bs2V1pmx7VNS8D\n24QQm4UQDuDD5FMzpboI7AEQQowBNwLndTh2UbOzs3p+nG7qcjUmVblAXbYuV2NaaVwtp2uklBkh\nxKeAHwBW4C+llMeFEJ8sbP8C8DvAXwshXgME8Gkppa4t8ng8en6cbupyNSZVuUBdti5XY1ppXLrk\n5KWUjwOPl/3uCyU/TwJv0+NY1ZRKqVmN2eVqTKpygbpsXa7GtNK4OuaJ10QiYTZCRXW5GpOqXKAu\nW5erMa00rnZV1xiu8fFxsxEqqsvVmOrlSmZyXAklSWZyDLnsjPc7yD+GYT5bu9Xlakz1cGVzkqnI\nIguJDG6HlQ2rnFgt1+f3q2OCvN/vV7L2tcvVmGpxnZmN8+Uj07x4KUQ6e7UybLTPzjtvHOG9N3tx\nO6ymsJmlLldjWo5rLp7mK69O89S5eULJq89ruh1WHti8ig/tGmPNgDEPbBrlV8cEeYfDYTZCRXW5\nGlM1rnQ2xxdfnuSfjgWA/Oj9hlVOeu0WpiIpZqJpvnRwiu+enOXX37qBO9YOtI3NbHW5GlM1rh+d\nmeOPnrtEMpN/TtPrtjPitjOfyOCPpPjeqSA/OjvHv9y9hp/b6dX9ztEovzomyPf395uNUFFdrsZU\niSuWyvKffniOY/4YVgHvvdnLz90yyog7f1HkpOTIZIS/emWKU4E4//H75/iV+9bzrokRw9lUUJer\nMZVzSSn58xev8PVCB+Ke9QN8/M7VbB12FQP5xYUkf3/Yz4/PzfPnL17BN5/gV9+yAZuOKRyj/OqY\ngddgMGg2QkV1uRpTOVc8leU/fv8sx/wxRnrt/OF7tvNv7l1XDPAAFiG4Y+0An3/Pdj6ya4ychM//\n9BLfOalv3fH14pkquh64pJT86YF8gLdbBP/vW9bzX962hW0jvdf01DescvLYQ5v4zYc302MV/OD0\nHP/t6QvkWnyYtBqXnuqYID84OGg2QkV1uRpTKVdOSn7v6QucnIkz1ufgD9+zjZtGqz8RaLUI/uVd\na/jUm9cB8MfPX+J534IhbCqpy9WYSrm+cSzAt07kA/xvPbKZd02MLJuGecumVfz+u7fRa7fwzPkF\n/s+LVwzh0lMdE+RXWllUq7oeuP7m4BQvXAzT32Pl9965lfH++ga8fnaHl4/fMU5Owu8/7WMqvKg7\nm0rqcjUmjevQlTD/56V8kP6NBzdy9/r6Hka6adTNbz2yBbtF8PVjAX5yfl5XLr3VMUE+mUyajVBR\nXa7GpHEd90f58pFpLAI++zObWOtprKLhF24f576NHuLpHL/74wuks0smPW2aTTV1uRpTMpkknMzw\n+8/4yEn4+dvGeOuWxnrRt6/p55fuWQvAHz57UZeOhFF+dUyQv55rcs2QylyJdJY/+IkPCXzw1rGm\nKmWEEPz7BzYw1ufg9Gycr702owubiupyNabx8XH+9MBl5uIZdoy6+dgdq5v6nJ/dMVLsSHz+pxdp\ndbJHo/zqmCDv9/vNRqioLldj8vv9/N9DfibDKTYPOvnoHc1/8ft7bPz7+zcA8LeH/VwJtdZTUtkz\nFaUq1xPHLvLjc/P0WAX/4a0bmn7ISQjBr75lPQM9Vg5PRnnybGtpG6P86pgg73Q6zUaoqC5XY1rI\n2Pjm8QAC+LUHNuKwtvYVvX1tPw9vGyKdlfzJgcstfZaqnnW56lcmJ/namfwf+4/fuZq1ntYYV7ns\nxbTNn714hVgq2/RnGeVXxwR5l8tlNkJFdbka0z+cipLJSd62fYjt3l5dPvOX7l5Dr93CK5cjHLoS\nrv2GKlLVsy5X/frnEwEmoxnWDPTw3psrrpbXsB7ZNsTOcTehZIavvjrd9OcY5VfHBPn5eX1GuPVW\nl6t+HZmM8MpkHKfNwi/uXqPb565y2fnwbWMAfPGlyaZrm1X0DLpc9Sq6mOFvD+dTIv/mnrXYW7xL\n1CSE4F/fne/Nf/3YDIFYc7NJGuVXxwT54eFhsxEqqstVv/72UP4C/OCtowz32nX97PfdPMqI287Z\nYIJnzjdXO6+iZ9DlqlffPB4gspjlZq+LezfoO+3FTaNuHti8ilRWFr/HjcoovzomyEciEbMRKqrL\nVZ+OTkU46o/SaxO8b+eo7p/fY7Pw0dvzg7hfPuJvqjevmmeauly1FUtl+UZh2oKfvcFlyIylv7h7\nNRYBT5yZYybaeG/eKL86JsivtIUAWpVqXH9XuI3es8Fp2CySD28bYqTXzoX5JC9ebDw3r5pnmrpc\ntfXN4wGiqSy3jvexxaApddZ5nNy/eRWZnOTrTZTsdhcNqSGVa3JVlEpcJ6ZjHJ6M0mu38PO7Nxh2\nHIfVwgduzd8l/P0Rf8N1zSp5Vqou1/JKZnJ841g+6H70jnFDuT6yK//Zj78+y3wi3dB7la6TF0K8\nQwhxSghxVgjxWJV9HhRCHBFCHBdCPKPHcUulak1ul6u2/qlwAf7sDi/R+YChx3rnxAgep41TgThH\np6INvVclz0rV5VpeT56dI7KY5UZvL7tW9xnKtWU4n+9fzEq+faKxCfKUrZMXQliBPwHeCewAPiKE\n2FG2zyrgT4GflVLeDHyg1eOWS8VyLehy1VIgluLZCwtYRP4JQqO5nDYL77kpPwXxt0409gdFFc/K\n1eWqLillcQ2CvYU54I3mev8t+Uqu756cJdXAdBoql1DeDZyVUp6XUqaArwCPlu3z88A3pJQXAaSU\nrT9jXqbrbYECs6UK13dOzJKTcP/mVYy4HW3hevdNI1gFPO8LNTRApopn5epyVdfBKxEuLiQZ7rVz\n/+b8/DRGc90y7mbLkJOFZIZn36i/kssoLj2C/FrgUsnry4XflWo7MCiEeFoIcVAI8XEdjnuNQqGQ\n3h+pi7pc1bWYyfHd1/O3tNqDKe3gGu6188CWQXKShuacV8GzSupyVdc3jxcqanaMFBf4MJpLCMGj\nO/Lf528dr/9u0Siudq0MZQPuBPYALuCAEOIFKeXp0p1mZmbYt28fNpuNbDbL3r172b9/P36/H7fb\njdVqJRwO4/V6mZubQ0qJ1+tlenoau91OMBgkGo0yNjZGIBBACMHQ0BCBQICBgQGy2SyxWIzx8XH8\nfj92ux2Px8Ps7Cwej4dUKkUikShudzgc9Pf3EwwGGRwcJJFIkEwmi9udTicul4v5+XmGh4eJRCKk\nUqnidpfLRU9PDz6fj5GREUKhEOl0uri9Vpv6+voADGlTOp0mHA431SaHw0EoFGq5TceiPYQXs2wa\nsLGpT+Dz+XA4HMzOzhp+nu4eyvDUOfjOiRnets5Kn6unZpvS6TTxeLyt56meNvX29jI1NWXYeWq2\nTRaLBZ/Pp+v11EibTl8J8PKlMHaL4BZ3orgoRzqdJplMGnqe3rx+lD+zCV4PxDl0YYZhkajZJrfb\nzeTkZFPnaTmJVmdOE0K8CfhtKeXbC68/AyCl/FzJPo8BLinlbxVe/wXwfSnlP5Z+1oEDB+TExERT\nHJOTk6xZo99Tknqpy1Vd/+6fT3N8OsavPbCBt28fbiuXlJL93zzF2WCCzzy0kYduGKr5HhU8q6Qu\nV2X9zcEp/vawn4duGOQzD21qO9efv3iFr702wztvHObf3V+7aqwVrkOHDh3cs2fP7krb9EjXvAxs\nE0JsFkI4gA8D3y7b51vAW4QQNiFEL3APcFKHYxeVTjdWrtQudbkq69JCkuPTMVx2Cw9sXlX8fbu4\nhBC848b8H5YfnJ6r6z1me1ZNXa6lyuYkPzyT77m/Y/u1T5K2i0v7fj1zfp5EuvbEZUZxtRzkpZQZ\n4FPAD8gH7q9KKY8LIT4phPhkYZ+TwPeBo8BLwBellMdaPXapVKnJLVeXq7J+eCYfWB/YvAqX/erD\nT+3keuiGQexWweErkboGYM32rJq6XEt1eDLCTDTNeL+DXWuuTWm0i2vDKic7Rt3E0zl+eqH2AKzS\ndfJSysellNullDdIKX+38LsvSCm/ULLPH0gpd0gpd0opP6/HcUulSk1uubpcS5XNSZ6o0stqJ1d/\nj437NnqQXP2js5y657Ixmcn1/VP579fbtg9jKZvCoJ1cb9ueTwP+sI67RWXr5FWR2119gWcz1eVa\nqlcuh5mLZ1jn6WHH2LUc7ebSxgJ+eDpYcz6b7rlsTGZxhZMZnveFEMDbti0da2kn11u3DNJjFbw6\nFa25RKBRXB0T5K1WY+Y7aVVdrqXScuBv2z60ZKKodnPdtqYfr9uOP5LitRpPwHbPZWMyi+snbyyQ\nyUnuXNfPaN/S2vN2crkdVu4vjDnVuls0iqtjgnw43PxiEEaqy3WtYqksL17K97Ie3rq0l9VuLqtF\nFDmeOr/8fN7dc9mYzOJ66lz+PD50Q+XFudvN9ci2/N3iU+fml50vySiujgnyXq8+q7zorS7XtTrg\nC5HOSm4Z72PEvbSXZQbXg4Vg8GyhB1hN3XPZmMzgCsRSHPNHcVgFb964quI+7ea6dXUfgy4bk+FF\nzgQTVfcziqtjgvzcXH1lcO1Wl+taPV3oLT9YpZdlBtfmIRcbB51EFrPLLg/YPZeNyQyuZ87NI4G7\n13uqTlndbi6rRfBAYUqFp89Vv1s0iqtjgnyrD3UZpS7XVYWTGQ5eDmMR8JZNnor7mOXXg1sKF+Ey\nq0Z1z2VjMoNLS7lVS9WAOVwP3pC/q3jm/HzVAX6juDomyHdvWRuTGVw/vbBAVsIda/tZ5aq8vJ9Z\nfj24JX8RPn9hgcVM5ZkDu+eyMbWb60ooyZnZBL12C3evr768nxl+3TTqZrTPTiCW5sR0rOI+3XRN\nDU1PN79KupHqcl3VM1qqZkv1XpZZfq31ONk24iKezvHypcopm+65bEzt5tIGXN+8aRU9tuqhzQy/\nLELwVi1lU2WA3yiujgny9UzUY4a6XHnNx9O8OhXFbhG8eWPlVA2Y65f2x+eZNypfhN1z2ZjazaU9\nVardlVWTWX6VDvBnKwzwG8XVMUG+K7X1/MUQuUKqpq+nXZOfNqa3FOqZX74Ubmixh67M11R4kfNz\nSXrtFm5bY9Airi1q67CLsT4H84kMr89UTtkYoY4J8tFoY0u5tUtdrryev5CfK/u+Tcv3ssz0a3V/\nD1uG8imbI5ORJdu757IxtZPruUIv/u71Azisy4c1s/wSQnBfoeDgOd/SueON4uqYID82NmY2QkV1\nufIPQB2ZjGARcO+G6gNiYL5fxYvwwtKL0Gy2aupyXQ2atToRYK5fGt/zvoUl1TRGcXVMkA8EjF0A\null1ufJz1aRzkh1j7qpVNZrM9ksbLzjgCy3Jm5rNVk0rnWs+nq9YsVsFd61bvhMB5vq1Y9SNx2lj\nMpziwnzymm1GcXVMkC+fA0UVdbmu3krfV+UJxFKZ7deWIRfj/Q4WkhlOluVNzWarppXO9fzFEBK4\nY00/vVUegCqVmX5ZLYI3bdDuFq99JsMoro4J8kNDtVf2MUMrnSuVzfFSoSRxuaoaTWb7JYTgvgLn\n82V5U7PZqmmlc2nB8s11pGrAfL+0lGC7vl8dE+RX+i1ro2oX19GpKPF0ji1DTlYP9NTcXwW/tLzp\ncxeuzZuqwFZJK5krP94TxSLgTTXGezSZ7dfta/px2S2cDSbwR65OP9xN19TQwEB9J7jdWulcWlVN\ntcmiyqWCXzcV8qZTkWvzpiqwVdJK5nr5UphMTnLzWF/N8R5NZvvlsFmKYwcHSnrzRnF1TJDPZmuv\noWiGVjKXlJLnLxZupetI1YAaflktolgF9OKlqxehCmyVtJK5Xiqcn1pVW6VSwa97C3n5F0uerjaK\nS5cgL4R4hxDilBDirBDisWX2u0sIkRFCvF+P45YqFmvfwwWNaCVznQsmmItnGOm1c8Owq673qOLX\nXYW5T14quQhVYSvXSuXKScnLl/PPM9yzvr5OBKjh1+51/QjgtalocZFvo7haDvJCCCvwJ8A7gR3A\nR4QQO6rs99+AH7Z6zErqLmbcmNrB9fLlfIC8a/1A3ZUDqvh159oBrAJOTMeILGYAddjKtVK5TgXi\nhJIZxvocrF9Ve7xHkwp+rXLZmRjtJZ2THJnMPwSl8kLedwNnpZTnpZQp4CvAoxX2+2Xg68CMDsdc\nou5ixo2pHVxaL/iuZWYELJcqfrkdVnaO95GTcLDQW1SFrVwrlUubSO6eDfV3IkAdv+5ar6Vs8ikn\no7j0mERkLXCp5PVl4J7SHYQQa4H3AQ8Bd1X7oJmZGfbt24fNZiObzbJ3717279+P3+/H7XZjtVoJ\nh8N4vV7m5uaQUuL1epmeniaVShEMBolGo4yNjREIBBBCMDQ0RCAQYGBggGw2SywWY3x8HL/fj91u\nx+PxMDs7i8fjIZVKkUgkitsdDgf9/f0Eg0EGBwdJJBIkk8nidqfTicvlYn5+nuHhYSKRCKlUqrjd\n5XKRyWTw+XyMjIwQCoVIp9PF7bXapE1YZESbIpEI4XC4qTY5HA5CodCybZoKLnByJoZVwKgMkUw6\n62pTOp1mdna27eepUpvuWN3Lq1NRnj07w64hiEQixOPxtp6netqUy+WYmppq6jwZ+d1bXFzE5/MZ\ndp6eOz8LwO3jLnw+X91tikQiJJNJ02PEBls+PfOib4EL6/JzJU1OTjZ1npaTaHWi+kJ+/R1Syn9V\neP0x4B4p5adK9vlH4H9IKV8QQvw18B0p5dfKP+vAgQNyYmKiKY5YLKbkqvUrlevpc/P816cusGt1\nH3/w7m3KcDUi33yCf/311/E4bfzDL+wkEY8rw1YqlTwrlZFc8/E0H/r7Yzisgq997Facy0wt3E6u\nRpSTkp//+2PMJTL82d4JRntyTXMdOnTo4J49e3ZX2qZHuuYKsL7k9brC70q1G/iKEOIC8H7gT4UQ\n79Xh2EXNzs7q+XG6aaVyvVTIxy+3eEMlqeTXhlVOxvochJIZTgfiSrGVaiVyaeM9u1b3NxTgQR2/\nLEJcM8BvFJceQf5lYJsQYrMQwgF8GPh26Q5Sys1Syk1Syk3A14B/K6X8pg7HLsrjqX90vZ1aiVw5\nKXnlUnNBXiW/hBBF/pcuhZViK9VK5HqpJB/fqFTy6+6SvLxRXC0HeSllBvgU8APgJPBVKeVxIcQn\nhRCfbPXz61UqlWrXoRrSSuQ6O5tgIZlhtM/OhlXOht6rml+lQV41Nk0rjSuTkxy8kh8Mr2dCsnKp\n5Ncda/uLVVzz0WTtNzQhXVZvkFI+Djxe9rsvVNn3F/U4ZrkSiYQRH9uyViKX9oDK3es8DU+6pJpf\nu9b047AKTs/GmV5wMDpqNtFSqeaZJqO4TkzHiKWyrPf01DVVRrlU8kur4np1KsrBK2E2r9O/jLIj\nnnj96tFp/vBIgkBMnb/QmlSoya0kI7lK6+MblWp+OW0WbhnPVzBM5swfrKsk1TzTZBTXKy18v0A9\nv7S7kfPx+qZlaFQdEeRfm4pyMpDg8JWlq/mYLVVqcstlFFc4meH1mTg2i+C2NY2vWamiX3eszS8n\nd+C8mhOBqegZGMd1qHCd37m2uSCvml93rutnzUAPLrlYe+cm1BFBXrsIDyoY5B0Oh9kIFWUU15Gp\nCBK4ecyNy157bu9yqeiXFkxOBDNLVvNRQSp6BsZwhZMZzszGsVsEt6xubuFr1fzaMuTirz+4g/ff\npOjAqwrSgvzhKxFyil2E/f1qLipsFJd2N6Wdk0alol+bh5wMumwsLOa4uGDM4FgrUtEzMIbryGS+\nE7FjzN1w6aQm1fzSxq2M4uqIIL9hlZNVPYKFZIYLc2pdhMFg0GyEijKKS7uVvn1Nc19YFf0SQhTb\nc0jBu0UVPQNjuA622ImAleUXdEiQF0JwW+HW7dCVcI2926vBwUGzESrKCK6p8CJTkRR9DivbRnqb\n+gxV/dKCiopBXlXP9OaSUracj4eV45emjgjyABOD+fzvoUm1LkKVyrVKZQSX5v1ta/qwWppbr1JV\nv7Sg8upUlHQ2ZzLNtVLVM725JsMppqMp+nusdU9dXUkrxS9NHRPktxXu3l6bipJS6CJMJtVKH2ky\ngutqPr75Xpaqfg277axxW0lmcksW+DZbqnqmN5d2l37bmv6mOxGwcvzS1DFBfvvGNWwedLKYlZyY\nVuciVK0mV5PeXNmc5PBka/l4UNcvgN2FR9BVq+JS1TO9uQ7pkI+HleOXpo4J8n6/X8m8qWo1uZr0\n5joXTBBZzDLW52DNQPMlaqr6BbDJlX/YTqXvF6jrmZ5c2ZzkyFR+cY1Wg/xK8KtUHRPknU5nMU2g\n0kXodDY2d0u7pDfXocn8rfQda/sbnsqgVKr6BXDLeB92i+B0IE44mTEbpyhVPdOT6/RsnFgqy5oB\nB6v7G5/KoFQrwa9SdUyQd7lc7Bx3Y7cIzsyqcxG6XM0PEBkpvbn0upVW1S+AwX43O8bcSPIPfaki\nVT3Tk6v4/VrT/HiPppXgV6k6JsjPz8/jsluVuwjn5+fNRqgoPbmSmRzH/TEE+UGxVqSqX5Bn0/6I\nHbkSNZnmqlT1TE8uvToRsDL8KlXHBPnh4WFAvXpmjUs16cl1zB8lnZPcMOzC42xtYlNV/YI8267V\nhSCvSCcC1PVML65EOsvJmRgWQVPzIZWr0/0qV8cE+UhEq9HOX4SvTqrR09K4VJOeXIeLD6i03stS\n1S/Is2339uKyW7gcWiQYS5uNBKjrmV5cx6djZHKSbSO99PW0Pjt6p/tVro4J8tpCANtHeum1W7gS\nXmRWgamHVVqgoFR6cr1aqHrY1WKqBtT1C/JsNotg51i+N6lKb15Vz/TierVQmruryQnJytXpfpWr\nY4K8VmNqtQh2Fub/PqJAb77Ta3JjqSxng3GsIj/zZKtS1S+4yqalDFS5W1TVM724tNJJLVXWqjrd\nr3LpEuSFEO8QQpwSQpwVQjxWYfsvCCGOCiFeE0I8L4TYpcdxS1VaY6r9xX9VgZ5Wp9fkHvNHyUm4\n0dvc1MLlUtUvuMqm3bGo0pNX1TM9uGKpLGdm41h06kRAZ/tVSS0HeSGEFfgT4J3ADuAjQogdZbu9\nAbxVSnkL8DvAn7d63HKVlh9pF6GWRjBTnV6udbTg8a063Uqr6hdcZbthyEWfw4o/ksIfMWahh0ak\nqmd6cB2f1joRvfQ6Wu9EQGf7VUl69OTvBs5KKc9LKVPAV4BHS3eQUj4vpdTqg14A1ulw3GtUuhBA\n6UU4HTE3/6baAgWa9OI66tc3yKvqF1xls5YsWKFCR0JVz/Tg0lKueqVqoLP9qiQ9gvxa4FLJ68uF\n31XTPuB7Ohz3GoVCoeLP116E5t5Sl3KpJD24tFtpvfLxoK5fcC2bNrX1qwrMeqqqZ3pwadevXoOu\n0Nl+VVLr9UgNSAjxEPkg/5ZK22dmZti3bx82m41sNsvevXvZv38/fr8ft9uN1WolHA7j9XqZm5tD\nSonX62V6ehq73U4wGCQajTI2NsZGV4YDwMFLC9zYE2VgYIBsNkssFmN8fBy/34/dbsfj8TA7O4vH\n4yGVSpFIJIrbHQ4H/f39BINBBgcHSSQSJJPJ4nan04nL5WJ+fp7h4WEikQipVKq43eVy0dPTg8/n\nY2RkhFAoRDqdLm6v1aa+vvwXW2tTIBBACMHQ0BCBQKClNqXTacLhcFNtcjgchEIhfCkXOQlbPFYs\nuQw+3+WW2+RwOJidnW37edLatNx5SqfTxONx5ubmGLfkyydfubTA7OwsQghDzlM9bert7WVqaqqp\nNhn53bNYLPh8vqbPk38uxNnZBDYL9KfmmJlJ6tKmdDpNMpls+3mq9d1zu91MTk421aZl426ra1YK\nId4E/LaU8u2F158BkFJ+rmy/W4F/At4ppTxd6bMOHDggJyYmmuKYnJxkzZo1xdfngwk++U+v43Xb\n+dsP39zSfCqtqJxLFenB9cWXrvDVozN8aNcY++7Sp42q+gXXsuWk5EN/d4xQMsNffeAm1nrMmw9F\nVc9a5TrgC/FbT5zn5jE3//M925XhMkqtcB06dOjgnj17dlfapke65mVgmxBisxDCAXwY+HbpDkKI\nDcA3gI9h4/H3AAAgAElEQVRVC/CtKp2+9sGUTUNOBnqsBGJppkzMy5dzqSI9uIr18TreSqvqF1zL\nZhGi2O4jJuflVfWsVS4jUjXQuX5VU8tBXkqZAT4F/AA4CXxVSnlcCPFJIcQnC7v9JjAM/KkQ4ogQ\n4pVWj1uu8hpTixDcqj2CbmLetFNrcuMlpW07RvXJx4O6fsFStmKQNzkvr6pnrXId1fEhu1J1ql/V\npEudvJTycSnldinlDVLK3y387gtSyi8Ufv5XUspBKeVthX8VbytaUaUa0+JDKyb2tDq1Jvf4dIyc\nLDxhrFNpG6jrFyxlK51Co9W0ZytS1bNWuMLJDOeCCewWoWsnAjrTr+XUMU+8ut1Lvwi7SiogzLoI\nK3GpoFa5jhp0K62qX7CUbZ2nh6FeGwvJDL4F85aUU9WzVrhe80eRwMSomx6bvmGqE/1aTh0T5K3W\npb3JDaucDLpszCUyXAqZ89BKJS4V1CrXq8WHoPS9lVbVL1jKJoTgtmJK0Ly7RVU9a4XLiPEeTZ3o\n13LqmCAfDoeX/E4IUXxIx6x65kpcKqgVrkQ6y2mdHzXXpKpfUJlNyxcfNTElqKpnrXBpd4p6TC1c\nrk70azl1TJD3er0Vf689KWdWXr4al9lqhUvLx2/TOR8P6voFldluLUyG95rfvLy8qp41yxVKZjg/\nl8RhFUx49U9hdJpftdQxQX5ubq7i70sHX824CKtxma1WuIy8lVbVL6jMtmbAwXCvnZCJeXlVPWuW\nS7srumnUjUPnfDx0nl+11DFBvloAXzvQY+pFaGbVxXJqhUu7ldZrvppSqeoXVGYrTQmalbJR1bNm\nuYr18TqXTmrqNL9qqWOCfLVbHbMvwk67NUyks5wOaPl4/YO8qn5BdbZbtJRNNyV4jZrl0u4UbzOg\nEwGd51ctdUyQn56errptl4kzBi7HZaaa5To+HSNbyMe7dc7Hg7p+QXW2YifCpLy8qp41wzWfSOOb\nT9Jjs3Cjt9cAqs7yqx51TJBfbqIebfD1qAl5+XomEDJDzXIV548fN6ZdqvoF1dnWe3oYdNmYT2S4\nbEKprqqeNcOl3Q3tGHVjtxoTnjrJr3rUMUF+Oa0ZcDBi8uBYp0jvRUI6QUKIYspGm1+/q+ak+WfE\noP5KVccE+Wi0+sVlZl5+OS4z1QxXIp3lVCCGRVBcR1dvqeoXLM9m5riPqp41w9WOTkQn+VWPOibI\nj42NLbvdrLx8LS6z1AzXiUI+fuuwMfl4UNcvWJ5NC0qvmZASVNWzRrlCyQwX5vP18dsNysdD5/hV\nrzomyAcCgWW331qSl8+18SKsxWWWmuFqRy9LVb9gebaNq5x4nDZm4+2f2lpVzxrles1fUh9vUD4e\nOsevetUxQb7WoiDX5OXn25eXN2uxklpqhkvv9VwrSVW/YHm2fF4+/3Rmu1M2qnrWKFe7xns6xa96\n1TFBfmhoaNntZuXla3GZpUa58vn4fH38Tp3nqymVqn5BbTazBl9V9axRLqMrtzR1il/1qmOCfD23\nOtoTdO3My3fKreHJmRiZnGTLkIu+HuOWBlbVL6gnJah1Ito7GZ6qnjXCFVnM8MZcfv74CZ3njy9X\nJ/jViDomyA8MDNTcRxt8fc3fvrx8PVxmqFEuI+erKZWqfkFtts1DLvp7rMxE0/gj7auXV9WzRriO\n+WNI4MbRXt3njy9XJ/jViDomyGez2Zr7rO53MOJub16+Hi4z1CjXawbNH18uVf2C2mwWIdg51v6U\noKqeNcJVnA/J4FQNdIZfjUiXIC+EeIcQ4pQQ4qwQ4rEK24UQ4o8K248KIe7Q47ilisVi9XC2vZSy\nHi4z1AhXMpPj9UAcAcXBRaOkql9QH9stJXeL7ZKqnjXC1Y5BfU2d4FcjajnICyGswJ8A7wR2AB8R\nQuwo2+2dwLbCv18C/nerxy1XvYvgXi2lbE/etBMWDT45nc/H3zBsbD4e1PUL6mMzY3BfVc/q5Yql\nspwLJrBZBDcZnI+H69+vRqVHT/5u4KyU8ryUMgV8BXi0bJ9Hgb+Reb0ArBJCrNbh2EXVuwjurpKL\nsB15+U5YNLidvSxV/YL62G4YctFrtzAVSRGItadeXlXP6uU6Ph0tLgrvshu/NN/17lej0iPIrwUu\nlby+XPhdo/u0JLvdXtd+q/sdeN12wovZtuTl6+VqtxrhKs7vbXA+HtT1C+pjs1pK5rFpU29eVc/q\n5Wr3fEjXu1+Nyth77wY1MzPDvn37sNlsZLNZ9u7dy/79+/H7/bjdbqxWK+FwGK/Xy9zcHFJKvF4v\n09PT2O12gsEg0WiUsbExAoEAQgiGhoYIBAIMDAyQzWaJxWLsHOvlqfMhfvL6FUZ3rWZ2dhaPx0Mq\nlSKRSDA+Po7f78fhcNDf308wGGRwcJBEIkEymSxudzqduFwu5ufnGR4eJhKJkEqlittdLhcWiwWf\nz8fIyAihUIh0Ol3cXqtN2qx09bRJ+0y73Y7H46nZpng8Tjgcrtmm2YUwr0/HEIAnPcfMzCIOh4NQ\nKGRImxwOB7Ozs021qZXzVE+b4vE48Xi8Zps2uDK8CBy6tMBWe6Sl81RPm3p7e5mammqqTUZ+97LZ\nLD6fr2abXp1MALDemSYcDrd8nmq1KR6Pk0wmdb2e9Pjuud1uJicnm2rTchKtzrMhhHgT8NtSyrcX\nXn8GQEr5uZJ9/gx4Wkr55cLrU8CDUsqp0s86cOCAnJiYaIrD5/OxcePGuvb9/qkgf/jsRd6yycNv\nPrylqeMZwdVO1ct1eDLCpx8/yw3DLv73+5o7N0ZwmaF62V6fifEr3z7NOk8Pf/mB8uEp87jarXq4\n4qkse//vUQC+8bFbdV8zuFkuM9QK16FDhw7u2bNnd6VteqRrXga2CSE2CyEcwIeBb5ft823g44Uq\nm3uBUHmAb1Uej6fufUsrbIzOyzfC1U7Vy/Vam2+lVfUL6mfbNtKLy27hcmiRYDxtMJW6ntXDdWLG\nuEXhq+l69qsZtRzkpZQZ4FPAD4CTwFellMeFEJ8UQnyysNvjwHngLPB/gH/b6nHLlUrVP8g1XsjL\nRxazXJgzNi/fCFc7VS9Xux6C0qSqX1A/m9UiuHmsffPYqOpZPVztmsqgVNezX81Ilzp5KeXjUsrt\nUsobpJS/W/jdF6SUXyj8LKWU+wvbb5FSvqLHcUuVSCTq3vfaenljSykb4Wqn6uFazOR4fSafj99p\nwHqulaSqX9AYWzvXfVXVs3q4zFiE5nr2qxl1zBOvjdaYlk49bKSu55rc12dipHOSzUMuBpztGaNX\n1S9ojK103VejpapntbiSmRynZ+OGLkJTSderX82qY4J8ozWmu9ZcvQiNzMtfzzW57U7VgLp+QWNs\n20d66bEKLi4kmU8Ym5dX1bNaXNpDdluGXIYtQlNJ16tfzapjgrzD4Who//E+B6N9xuflG+Vql+rh\n0u5ybmljkFfVL2iMzW61sKOQlzd6igNVPavF1c6H7Ep1vfrVrDomyPf3N/agTn5+eW3qYePy8o1y\ntUu1uFKZHCcD+bk02jkopqpf0DjbLYXvl9F5eVU9q8V1tHin2F7+69WvZtUxQT4YDDb8nnZMVtYM\nVztUi+v1QIx0VrJ50Nm2fDyo6xc0zrarTfPYqOrZclypTI7XA4VBfYMnvSvX9ehXK+qYID84ONjw\ne25tw/zyzXC1Q7W4jkwWellr2tvrUdUvaJztRm8vDqvgjfkk4WTGICp1PVuOq9iJGHLRb/Ckd+W6\nHv1qRR0T5JspPyrNy78xZ0z50vVarmVGaRuo6xc0zuawWoqzKhqZl1fVs+W4XjXp+wXXp1+tqGOC\nfDLZ+OBpvl7e2FLKZrjaoeW4tHy8oL35eFDXL2iOrR2Tlanq2XJcZjwEpel69KsVdUyQb7bG1Oi8\n/PVYk3ty5uqtdDvz8aCuX9AcWzvGfVT1rBpXKpvj5Ex+UL+dlVuarje/WlXHBPlma0yNzstfjzW5\nZtTHa1LVL2iO7aZRN3ar4PxcwrC8vKqeVeM6HYiTyko2DjrxtLkTAdefX62qY4K80+ls6n3j/T2M\n9TkMy8s3y2W0luMyKx8P6voFzbE5bBZ2FPLyRj39qqpn1bjMTNXA9edXq+qYIO9yuZp+760G3lK3\nwmWkqnGV5uNvMeEiVNUvaJ6tmLKZNCbIq+pZNS6zHoLSdL351ao6JsjPz883/V4j86atcBmpalxm\n5uNBXb+geTatDNWoh+5U9awSVyYnOTFdyMeb1JO/nvzSQx0T5IeHh5t+r9ajOGZAXr4VLiNVjcvM\nfDyo6xc0z6bVy1+YT7JgwDw2qnpWievMbJxkJsc6Tw9DveYsw3c9+aWHOibIRyLN95KMzMu3wmWk\nqnGZWb8M6voFzbM5rBZD55dX1bNKXEcmtfWCzfl+wfXllx7qmCDf6oT72pfuiM550+tpgYLS+ePN\nupVW1S9ojW1XcZ4k/YO8qp5V4tLaf1ubn6Qu1fXklx7qmCDfao3prQbNM3I91eSeLMwfv2XYnHw8\nqOsXtMZm5LiPqp6Vc6WyOY6bPOgK149feqljgnyrNaal9fLZnH55+eupJtfM0klNqvoFrbFt9/bS\nY7NwcSHJnM7rvqrqWTnXqUCcxaxk06CTQZc5+Xi4fvzSSy0FeSHEkBDiCSHEmcL/S2bYEUKsF0I8\nJYQ4IYQ4LoT41VaOWU2tlh9pefloSt+8/PVUrmX2oCuo6xe0xma3WthZyMvr3ZtX1bNyrqv5eHOn\n+r1e/NJLrfbkHwOelFJuA54svC5XBvg1KeUO4F5gvxBiR4vHXSI9Jtw34pb6elmgwIz1XCtJVb+g\ndbbiamQ6l1Kq6lk5l/acwG1rzPt+wfXjl15qNcg/Cnyp8POXgPeW7yClnJJSHir8HAFOAmtbPO4S\nhUKhlj/j6kWoX5DXg8sIlXOpkI8Hdf2C1tmMGnxV1bNSrsVMfr4agbnpQLg+/NJTrV7NY1LKqcLP\nfmBsuZ2FEJuA24EXK22fmZlh37592Gw2stkse/fuZf/+/fj9ftxuN1arlXA4jNfrZW5uDiklXq+X\n6elp7HY7wWCQaDTK2NgYgUAAIQRDQ0MEAgEGBgbIZrPEYjHGx8fx+/3Y7XY8Hg+zs7N4PB7W2hcB\nOOqP8MaFCzh7eujv7ycYDDI4OEgikSCZTBbf73Q6cblczM/PMzw8TCQSIZVKFbe7XC56enrw+XyM\njIwQCoVIp9PF7bXa1NeXvxhaaVMqlSKRSBS3OxwO+vv7SafThMPhYpsO+vN/77cOWAiHw8u2yeFw\nEAqFDGmTw+Fgdna2qTa1cp7qaVM6nSYejzd9nhyxKC6bhcuhRY6fv8SmsSFd2tTb28vU1FRbz1M9\n3z2LxYLP52N8fJynjl/KLwo/2MOc/wrSwPNUq03pdJpkMqnr9aTHd8/tdjM5OdlUm5aNu7LGwz9C\niB8BlYZ9Pwt8SUq5qmTfeSllxZnvhRB9wDPA70opv1FpnwMHDsiJiYma0JU0OTnJmjVrmnpvqT7+\nD8fxR1L8yXtvZNtIb8ufpxeX3irn+vffOc0xf4zffmQzb964apl3tpdLJenB9tnvn+Ply2E+/eBG\n9mwdUobLCJVy/dXLk3z51Wnef8sov3SP7jfyTXOppFa4Dh06dHDPnj27K22rma6RUj4spdxZ4d+3\ngGkhxGqAwv8zlT5DCGEHvg78XbUA36rSaX0qFvTOy+vFpbdKuZKZHKdm4qbWx2tS1S/Qh82IeWxU\n9ayUS4VBfU3Xg196qtWc/LeBTxR+/gTwrfIdhBAC+AvgpJTyD1s8XlXpVWN6tV5en8Gx66Em95g/\nSjon2TrS/qXYyqWqX6APW3Hcx6/f4Kuqnmlc8VSWU4EYFgE7Te5EgPp+6a1Wg/zvAY8IIc4ADxde\nI4RYI4R4vLDPfcDHgJ8RQhwp/HtXi8ddIr1qTLXBsdf8MV3q5a+HmlyttO12E59C1KSqX6AP29bh\nXnrtFibDKWai+jzhqKpnGtex6ShZCdtHenE7rCZTqe+X3mqp2yalDAJ7Kvx+EnhX4eefAqKV49Qj\nt1ufFd9H+xys7ncwFUlxfi7Rcl5eLy69Vcp16Eo+yJv5qLkmVf0CfdisFsEt4328eCnMq1MRHtnW\n+qRUqnqmcV0tnTT/+wXq+6W3OuaJV6tVvx6CnvPL68mlpzSucDLDuWACu0UocSutql+gH5veeXlV\nPdO4jkyZPylZqVT3S291TJAPh8O6fVaxnnmy9bypnlx6SuM6MhVBAjvG3Dht5n8dVPUL9GPTerRH\npiLUqm6rR6p6Fg6HiSzmOxE2i+BmBToRoLZfRsj8q1oneb1e3T5Lz3ls9OTSUxrXkSv53qQK+XhQ\n1y/Qj23LsIuBHisz0TRXwostf56qnnm93sLayTAx2qtEJwLU9ssIqeG6Dpqbm9Pts7S8fDyd41yL\n89joyaWnNK5D2qDrWjWCvKp+gX5sFiGKf1S18ZBWpKpnc3NzHNbGe0yer6ZUKvtlhDomyOtx21uq\nYilliykbvbn0kpSSmWiKyfAivXYL23V48EsPqeoX6Mt2R+GP6kEdgryqnkkpi+27U5FOBKjtlxHq\nmCCv962OXvOMqHxreLhkVkCrxfACqLqkql+gL9sdaweA/LhPp6YE6V3F5VC+E3HjqDoVLar61U3X\n1ND09LSun6dXXl5vLr00PT1dTBWokqoBdf0CfdnG+h2sHeghns7xeiDW0mep6tkzp/LTWu1a049N\nkU4EqOuXUVwdE+TrmainEY32OVgzkM/Lnw3Gm/4cvbn0ktvtLnkISh1GVf0C/dm0lE2reXlVPTsT\nyneOVErVgLp+GcXVMUHeCGmDY69cVnPh31Z0OZJmPpFhqNfGhlVOs3FWpPQK8ioqJyXHAkngaju7\nMkcdE+SjUf3XzrxzXT5vevBy8/WrRnDpIa1Nt6/pJz+9kBpS1S/Qn+22Nf1YRH4u/1gq2/TnqOjZ\nuWCCSCrHaJ+dtQM9ZuNcIxX9AuO4OibIj40tO5V9U7q9cBGeaOEiNIJLD52L5k+9KvXxmlT1C/Rn\nczusTHjd5GRrC9Wo6NnBK/lOxJ1rB5TqRICafoFxXB0T5AOBgO6f6XZY2TGavwgPN1lKaQRXq0pl\nchz154OKdreiilT0S5MRbHqkbFT0TGuPiqkaFf0C47g6Jsgb1VtoNWWjWi8G8rMCprKwZcjFcK/d\nbJxrpKJfmoxguxrkm08JquZZMpPjuD+/1J9qd4qgnl+ajOLqmCA/NKTPKjvl2r3u6uBrMw8rGMXV\nirSB5LvWqXcBquiXJiPYJkbd9NotXAotEog1N/Wwap69NpVfn+CGIaep6wVXk2p+aTKKq2OCvFG3\nOluHexnosTIdTTU1z4iKt4YvF+5KdiuWqgE1/dJkBJvNIorPZBxssopLNc+0u5Jt6n29APX80tRN\n19TQwIAx3yirRRRvqZsppTSKq1nNRFP45pM4bYIdY+o8hahJNb9KZRSb9sf2lSZTgqp5pk1lsHu9\nx2SSylLNL01GcXVMkM9mmy9Bq6XdLeTljeRqRlobbh5xYreqd/pV86tURrHdpX2/rkTINPF0tUqe\nzURTXJhP0mu3sG1QrfEeTSr5VSqjuFq6yoUQQ0KIJ4QQZwr/Dy6zr1UIcVgI8Z1WjllNsVhrj4Yv\npzsL84wcmYqSyuYaeq+RXM3o5cLdyMQqNQefVPOrVEaxrR7oYb2nh1gqy4npxkspVfLspUv5TsQd\na/tZTDT/pLiRUsmvUhnF1WpX7jHgSSnlNuDJwutq+lXgZIvHqyojF+cddtvZPOhkMZPj+HRjJ0Kl\nRYMzOVnMlz44scZkmspSya9yGcl2z4Z8akMLko1IJc9evBgC4K71HqW4SrXSuFoN8o8CXyr8/CXg\nvZV2EkKsA94NfLHF41WV0YvzaqWULzd4Eaq0aPDrMzHi6RzrPD0QU3NObZX8KpeRbHetz3+/mgny\nqniWyuSK8yHdvW5AGa5yrTSuVoP8mJRyqvCzH6j2yNbngd8AGst1NCC73dj8392Fi1DrqdQro7ka\nkVZVc9e6AaW4SqUqFxjLtnPMjctu4cJ8kploY6WUqnh21B9lMSvZOuxi2G1XhqtcK42rZhGrEOJH\nQKX7iM+WvpBSSiHEklEjIcS/AGaklAeFEA8ud6yZmRn27duHzWYjm82yd+9e9u/fj9/vx+12Y7Va\nCYfDeL1e5ubmkFLi9XqZnp7GbrcTDAaJRqOMjY0RCAQQQjA0NEQgEGBgYIBsNkssFmN8fBy/34/d\nbsfj8TA7O4vH4yGVSpFIJIrbHQ4H/f39BINBNnhW0WsTXAot8kYggiU+h9PpxOVyMT8/z/DwMJFI\nhFQqVXy/y+XCYrHg8/kYGRkhFAqRTqeL22u1SZuVTq82/fTcLAA3j9iJxyOEw2ESiQTJZLL4/nra\n5HA4CIVChrTJ4XAwOzvb9HkaHBw0rE3xeJx4PG7YeZoYtHJ4Jsf3jrzB224YqLtNvb29TE1NtfU8\nVWrTj0/nV1HbPiBZWFggm83i8/nafp5qtSkej5NMJnWPEa22ye12Mzk52VSblo3hraxGIoQ4BTwo\npZwSQqwGnpZS3li2z+eAjwEZwAkMAN+QUn60/PMOHDggJyYmmmLx+Xxs3LixqffWq//64zd4+vwC\nn7x3LXt3jirDVY9moik++pXjOG0WvvbRW5i6ckkJrnKp4lclGc32vVNB/uezF7l3wwD/5W03KMNV\nj6SU/OJXTzAVSfH592xnx5hbCa5K6kSuQ4cOHdyzZ8/uSttaTdd8G/hE4edPAN8q30FK+Rkp5Top\n5Sbgw8CPKwX4VuXxGF+Te29hcOyFBlI27eCqRxrz7nX9OGwWZbjKpSoXGM92d2Hc5/BklFSm/sym\nCp5dDi0yFUnR32PlRm9+KUkVuCpppXG1GuR/D3hECHEGeLjwGiHEGiHE463CNaJUqrlHwhvRXesH\nsIj8Y9v1zkrZDq56pAV57Q+VKlzlUpULjGcbdtu5YdjFYskEcvVIBc+0AePd6waKS0mqwFVJK42r\npSAvpQxKKfdIKbdJKR+WUs4Vfj8ppXxXhf2fllL+i1aOWU2JRMKIj71G/T02do71kZX1V9m0g6uW\nYqksRyajWMTVAWQVuCpJVS5oD5vWm29kgF8Fz7ROxD3rrz61qQJXJa00LvUeeWxS7ap9vXdD/ktc\nb8pGhZrcg1fCZHKSm0bdrHLlR/BV4KokVbmgPWxv3pS/03rOF6p7QjyzPQsnM7zmj2IVV0tBwXyu\nalppXB0T5NtV+3rvxvxF+PLlcF0LfKtQk/vCxfxdx5s2XM35qcBVSapyQXvYto30MtJrZzaW5vRs\nfU+Mmu3ZCxdD5GR+we7+nqsFe2ZzVdNK4+qYIO9wONpynHUeJ+s8PUQWs3U9/dourmrK5iQvafn4\njVeDvNlc1aQqF7SHzSLE1d78hfruFs327DlfnvPNG68dODSbq5pWGlfHBPn+/vbNja4NXj7nW6i5\nbzu5KunETIzwYpa1hflRNJnNVU2qckH72O7buAqA5y7U/n6BuZ4l0tnipHflQV7Vc7nSuDomyAeD\nwbYd6y2b8hfhT99YqJk3bSdXJWmB4t4N1661aTZXNanKBe1ju2V1H/09Vi6FFrm4kKy5v5meHbwS\nIZWVTHh7GXFf2xNV9VyuNK6OCfKDg1UnwNRdE6O9jLjtBGJpTgWWz5u2k6tcOSl59o18kL9/87Uc\nZnItJ1W5oH1sNosoTlhWT2/eTM+eL/BpKaZSqXouVxpXxwT5dpZFWYTg/kJv/idvLH8RmlmudSoQ\nJxBLM+K2MzHae822lVZGpofayXZfIfXxvK92Xt4szzI5WRzU11JMpVL1XK40ro4J8slk7dtaPXX/\n5vyX+tkaKZt2c5XqJ+fngTyrpWyRYDO5lpOqXNBetjvXDdBjFYU/1Ms/JGOWZ69ORoimsmxY5WT9\nKueS7aqey5XG1TFBvt21rzvG3Az12piOpjgTrP4X2KyaXCklzxZupR/YvLSXtdJqhfVQO9mcNkux\n5vwn55e/WzTLs6cLnYhK3y9Q91yuNK6OCfLtrn21CFEcgH12mZSNWTW5rwfizETTjPTauWl06Vqu\nK61WWA+1m+3BLfkcrRZMq8kMz1LZXLHE861bKgd5Vc/lSuPqmCDvdC69XTRaDxRTNvNVUzZmcAEl\nA65LUzVgHlctqcoF7We7Z4MHl93CqUCcK6Hqt/JmeHbwcj5Vs2XIycZBV8V9VD2XK42rY4K8y1X5\ni2akbh7rY9BlYzKcqvp0ohlcOSn5yRvL30qbwVWPVOWC9rP12CzF2vOnlknZmOGZdnfx4A3VK0JU\nPZcrjatjgvz8/PK3tEbIahHFL/mPzlQ+vhlcr01FmYmmGe2zc9PY0lQNmMNVj1TlAnPYHip8v54+\nV/1usd1cyUyOAz4tVVM9yKt6LlcaV8cE+eHhYVOO+/DWISDfs8lUmMvGDK4fnc2v37pn61DFVA2Y\n51ctqcoF5rDdsXaAgR4rFxeSnJ+rPMDfbq4XL4ZIZnJMeHtZ3d9TdT9Vz+VK4+qYIB+JREw57tZh\nFxtXOQklM7xyeen0w+3mSmZyxXy89geokszyq5ZU5QJz2GwWwQOFB9meOle5p9durh+dyXcilkvV\ngLrncqVxdUyQN2shACEEP7M1/2V/svDlL1W7uQ74QsTTOW709lasXda00hZO0ENmsT1U+H796Oxc\nxZlP28kVjKd5+XIYq4CfqRHkVT2XK42rY4K8mbWvewo95ucvhpasGNVuLq2X9ci26r14WHm1wnrI\nLLadY27WDvQwF8/wcoW7xXZyPXlmjpzMT9KnrU1QTaqey5XG1TFB3sza19E+B7tW95HOyiU1ze3k\nmounOXgl38tabkAMVl6tsB4yi00IwTtuzOdrv39q6SRW7eKSUvL90/njv/3G2vljVc/lSuNqKcgL\nIYaEEE8IIc4U/q8YWYQQq4QQXxNCvC6EOCmEeFMrx60ks8uitIvwuydnr6mCaCfX908Fi70sj9O2\n7DPI77cAABIwSURBVL5m+1VNqnKBuWyPbBvCIvKDnnPx9DXb2sV1cibO5dAiQy4bd60bqLm/qudy\npXG12pN/DHhSSrkNeLLwupL+F/B9KeUEsAs42eJxl8jshQDu37SKgR4rZ4OJa2rm28WVzUkePzUL\nwLtvGqm5v9l+VZOqXGAu21CvnXvWe8jKqyk5Te3i0u4iHt42VFysezmpei5XGlerQf5R4EuFn78E\nvLd8ByGEB3gA+AsAKWVKSlnfaggNKBSqf+FjI+SwWXjb9nxv/jsnZ4u/bxfXK5fDzETTrBlwcMfa\n2osPmO1XNanKBeazFVM2p4PkSu4W28EVTmZ46lz+j8vbt9dX6me2X9W00rhaDfJjUsqpws9+YKzC\nPpuBAPBXQojDQogvCiEqP6HTgkZGavdejda7J/Jf/qfPLxBdzADt49L+sLxrYqRqbXypVPCrklTl\nAvPZ7l4/wEivncuhRQ5duVpu1w6uH5wOspiV3Lm2f9mqrVKZ7Vc1rTSu5RO3gBDiR0ClYd/Plr6Q\nUkohRKVH8mzAHcAvSylfFEL8L/Jpnf9UvuPMzAz79u3DZrORzWbZu3cv+/fvx+/343a7sVqthMNh\nvF4vc3NzSCnxer1MT0+TSqUYGhoiGo0yNjZGIBBACMHQ0BCBQICBgQGy2SyxWIzx8XH8fj92ux2P\nx8Ps7Cwej4dUKkUikShudzgc9Pf3EwwGGRwcJJFIkEwmi9udTicul4v5+XmGh4exxCPcNGTj5FyG\nf3jpHO+5cZBIJILNZmNkZIRQKEQ6nS6+v1ab+vr6AGq2yTcb5uXLYWwCbu1PE4vFarbpwoULbNiw\noWabIpEIqVSquN3lcuFwOAiFQoa0KZ1Os2rVKkPPU7NtunLlClu2bGn6POnRpnfdOMjfHJ7hKwcv\ns31gHfPz8+RyOZxOp2HnacQ7yj+9lh8UfMfWAXw+X11tmpmZoaenp+3nqVabpqam2Lp1qykxYrk2\nSSmbbtOyMbzW8nXLvlmIU8CDUsopIcRq4Gkp5Y1l+4wDL0gpNxVe3w88JqV8d/nnHThwQE5MTDTF\n4vP52LhxY1Pv1VPPXVjgP//oDcb6HPz1B3dw+dJFw7n+9MBlvnk8wMNbB/mNBzfV9R5V/CqXqlyg\nBls4meEXvnyMxazki++/iQ2rnIZzHfCF+K0nzrO638FffmBHXfl4UMOvSupErkOHDh3cs2fP7krb\nWk3XfBv4ROHnTwDfKt9BSukHLgkhtOC/BzjR4nGXSJXa1zdt9LDO08N0NMVP3lgwnCuczPC9woDY\n+2+plC2rLFX8KpeqXKAG24DTxp7CMxDfPB4AjOf6xrEZAN5z00jdAR7U8KuSVhpXq0H+94BHhBBn\ngIcLrxFCrBFCPF6y3y8DfyeEOArcBvzXFo+7RKrUvlqE4OduGQXgH49OMzU1VeMdrem7r8+ymMmx\ne10/W4brL8FSxa9yqcoF6rC972YvAE+cDjIfTxvKdXw6yqtTUdwOK++caCxnrIpf5VppXC0FeSll\nUEq5R0q5TUr5sJRyrvD7SSnlu0r2OyKl3C2lvFVK+V4ppe7Trbnduo/lNq1Htg6xymnjbDDBG/Ga\nwx5NK5nJFXtz7y/8YalXKvlVKlW5QB22jYMu3rTBw2JW8o+vzRjK9eUj0wA8umMEt8Pa0HtV8atc\nK42rY554tVob+wIaKYfNwvt25ntbX389vOwasK3o2ycCzCcybB/p5fY1tcsmS6WSX6VSlQvUYvuF\nO/K39v98cpZYxphjnJ2N89KlMD02C+/b2VgnAtTyq1Qrjatjgnw4vHRODzP16A4vHqeN03OLvHhJ\nf7ZYKss/vJrvZf3i7tWIOsomS6WaX5pU5QK12LaP9HLP+gEWMzm+dixgyDH++mA+1fjuieGaT1BX\nkkp+lWqlcXVMkPd6vWYjXKNeh5UP78oPhP71K5PXPLyih75xbIbIYpad427urOPhp3Kp5pcmVblA\nPbaPFnrzT11aZDqi7wyGhycjvHQpTK/dwod21T+gXyrV/NK00rg6JsjPzS2d5tdsveemEQadFs7P\nJXmiwjTEzWommuKrR/MVD79455qGe/Ggpl+gLheox3aj181DNwySykq++PIV3T43JyVffCn/eR+8\ndYzBGrNNVpNqfmlaaVwdE+SNynu3IofNws9t6wXgiy9NEk7qkzz9sxevsJjJcf/mVdy6uvbDEJWk\nol+gLheoybbvrjXYLfDM+QWO+aO6fOb3TgU5M5tguNfO3gYH9Eulol+w8rg6Jsiregv2nlvXcet4\nH6Fkhr98ZbLlz3vlcphn31igx2bh39yztunPUdUvVblATbbRPgd7b86XNn7+p5dIZXItfV4wnuaL\nL+W/p5+8dy1OW/MhQkW/YOVxdUyQn56eNhuhomZmZvjl+9Zhswgefz3IS5ean4QonMzwP35yEYCP\n3j7OaF/zs9ap6peqXKAu2wPeLOs8PVxcSBYHS5uRlJI/+uklYqksd68f4IHNq1riUtWvlcbVMUG+\nnjkczFBfXx8bB1184s7VAPz3Zy4yXzYfeD2SUvL5n14kGE+zY9TdcF18JS4VpSoXqMs25OnnP7x1\nIxYBX39thkNXmqvS+KfjAQ5cDOF2WPnlN69vaqynVKr6tdK4OibIq64P3DrKrtV9LCQz/PaPzrPY\n4G313x2Z5qcXQvTaLXz6oY0NPV7eVefrplE3P3/bOBL43R9f4EposaH3vzoZKaZpfu3+DYz1qznn\neleNq2OCfDSqz6CT3tK4LELwmYc2Mdpn5+RMnM89dYF0tr5A/73XZ/mbg1MI4NMPbmJ1f49uXKpJ\nVS5Ql03j+ugd49y7YYDIYpbP/uAsM9H6yirPzsb5rSfOk8lJ9u708pYW0zTlXKpppXF1TJAfG2uu\nltdolXIN9dr5/95+A26Hled9IX7zh+eL885XkpSSr746zf/86SUgPxD2po0e3blUkqpcoC6bxmUR\ngk8/uImtwy4mwyl+/btneGMusex7D10J8+vfPUM8neOBzav413c3P5hfjUs1rTSujgnygYAxT/21\nqnKuTYMufv9dW/E4bRy8EuGXvvE6z76xsORhqQvzCT77g3N88eX8LfT/c+/aph4tr5dLFanKBeqy\nlXK5HVb+27u2cqO3F38kxa986xT/8Oo0ybL04EIizf9+4TKf+d454ukcb928it94UN804PXgl0oy\nisu42bParFYHiYxSJa5tI738r5/dzueeusCpQJzfefINRtx2bh51Y7dZ8M0nODOb74H1Oaz82gMb\nuG+TPrfQy3GpIFW5QF22cq7+Hht/8O5t/PFzl/jhmTn+4uVJ/v6In1vH+/A4bQRiKY5Nx0hnJRYB\nP3/bOB+7Y7yuFcVa4VJFK42rY4L80NCQ2QgVVY1rzUAPn3/Pdr5zcpZ/fG2amWiaZ964uvSt02bh\n4W1DfOz2cQZ7m3visBkus6UqF6jLVonLabPw62/dyIM3DPJ/D01xcia+ZA6le9YP8LE7V7N9pLdt\nXCpopXF1TJAPBAJKrvayHJfVInj0Zi/v2THC+WAC30KSVFYy3ufgpjF3Sw+itMJlplTlAnXZluPa\nvW6A3esGmAovcjaYILqYYcBp4+YxN6uanK5ADy4ztdK4OibIDwwMmI1QUfVwWYRg60gvWw3qUVXS\n9eyXWVKVrR6u1QM9rB5ovSqrEV3Pfpkho7g6ZuA1m82ajVBRXa7GpCoXqMvW5WpMK42rY4J8LBYz\nG6GiulyNSVUuUJety9WYVhpXS0FeCDEkhHhCCHGm8P9glf3+nRDiuBDimBDiy0IIZyvHraSVtjhv\nq+pyNS5V2bpcjWmlcbXak38MeFJKuQ14svD6Ggkh1gK/AuyWUu4ErMCHWzzuEq20xXlbVZercanK\n1uVqTCuNq9Ug/yjwpcLPXwLeW2U/G+ASQtiAXqD1OXfL9M1vflPvj9RFXa7GpCoXqMvW5WpMK42r\n1eqaMSmlNrepH1jyXK6U8ooQ4r8DF4EE8EMp5Q8rfdjMzAz79u3DZrORzWbZu3cv+/fvx+/343a7\nsVqthMNhvF4vc3NzSCnxer1MT09z4sQJgsEg0WiUsbExAoEAQgiGhoYIBAIMDAyQzWaJxWKMj4/j\n9/ux2+14PB5mZ2fxeDykUikSiURxu8PhoL+/n2AwyODgIIlEgmQyWdzudDpxuVzMz88zPDxMJBIh\nlUoVt7tcLg4fPozP52NkZIRQKEQ6nS5ur9UmbVY6I9q0sLBAOBxuqk0Oh4NQKGRIm44ePcrs7Gzb\nz1M9bVpYWCAej7f1PNXTphdeeIEPfvCDbT1P9bTp7Nmz+Hy+tp+nWm1aWFggmUwqEyO0Nj333HN8\n4AMfaKpNy0nUWo1ECPEjoFKy6LPAl6SUq0r2nZdSXpOXL+Tpvw58CFgA/hH4mpTyb8s/8MCBA3Ji\nYqImdCXdd999PPfcc02910h1uRqTqlygLluXqzF1ItehQ4cO7tmzZ3elbTWD/HISQpwCHpRSTgkh\nVgNPSylvLNvnA8A7pJT7Cq8/Dtwrpfy35Z/35JNPBgBfMyxzc3MjQ0NDs82810h1uRqTqlz/f3tn\nFyJlGcXx3x9NIbOijwtTc3fBAq/MIvZCvVkrlTIqiI2goiCCiCQiDCG81KIuIkiKJApLIZK8KAgh\n6kpLZf0otd01qZZtBbswKPo8XTxn9N1p3sHZcZ7ZXs4PXuZ5z7zzPv89zzNnn485MzB9tYWu1qio\nrkUDAwMNf1qq3eWa3cDDwGZ//KjBNd8D/ZIuJS3XDAD7G92sTGQQBEEwNdrdeN0M3CZpGFjl50i6\nTtLHAGa2D/gAOAgc8TrfaLPeIAiC4AJoa7kmCIIgmN5UIuNV0mpJJySNSPrPZ/U7XPdCSZ9J+sYT\nvp52+yZJY5KG/FhbeM3zrvWEpDs6qO2UpCNe/363lSaw5dAl6caCT4YknZW0vhv+krRN0mlJRwu2\nlv0j6Wb384ikV9Xmd8aW6HpJ0nFJhyXtknSl23sk/Vbw29bMulput0y6dhY0nZI05Pac/iqLDXn7\nmJn9rw9SctUo0AfMAg4BSzLWPw9Y5uW5wLfAEmAT8GyD65e4xtlAr2uf0SFtp4Br6mwvAhu8vAHY\nkltXXdv9BCzqhr+AlcAy4Gg7/gG+BPoBAZ8Aazqg63Zgppe3FHT1FK+ru08OXS23Ww5ddc+/DLzQ\nBX+VxYasfawKI/lbgREzO2lmfwA7SElaWTCzcTM76OVfgGNAs99QuxvYYWa/m9l3wAjpb8hFWQJb\nN3QNAKNm1uwTVR3TZWZfAD83qO+C/aP0qbLLzWyvpXfjO5QnBU5Zl5l9ama134rcCyxodo9cuprQ\nVX/V8BHv/cD7ze7RIV1lsSFrH6tCkJ8P/FA4/5HmQbZjSOoBbgL2uekpn15vK0zJcuo1YI+kA5Ie\nd1tZAls3/DjI5Ddft/0Frftnvpdz6QN4lDSaq9HrSw+fS1rhtpy6Wmm33P5aAUyY2XDBlt1fdbEh\nax+rQpCfFki6jJT0td7MzgKvk5aQlgLjpCljbpab2VJgDfCkpJXFJ31U0JWdd0mzgHWk5DiYHv6a\nRDf9U4akjcBfwHY3jQPXezs/A7wnKecXpk+7dqvjASYPJLL7q0FsOEeOPlaFID8GLCycL3BbNiRd\nQmrE7Wb2IYCZTZjZ32b2D/Am55cYsuk1szF/PA3scg0TPv2rTVFP59blrAEOmtmEa+y6v5xW/TPG\n5KWTjumT9AhwJ/CgBwd8an/GywdI67g35NI1hXbL6a+ZwL3AzoLerP5qFBvI3MeqEOS/AhZL6vXR\n4SApSSsLvub3FnDMzF4p2OcVLrsHqO387wYGJc2W1AssJm2qXGxdcyTNrZVJG3dHOZ/ABpMT2LLo\nKjBphNVtfxVoyT8+7T4rqd/7wkM0TgpsC0mrgeeAdWb2a8F+raQZXu5zXScz6mqp3XLpclYBx83s\n3FJHTn+VxQZy97F2do+nywGsJe1cjwIbM9e9nDTdOgwM+bEWeJeU/HXYG29e4TUbXesJ2tzBb6Kr\nj7RTfwj4uuYX4GrS10IPA3uAq3Lq8nrmAGeAKwq27P4i/ZMZB/4krXM+NhX/ALeQgtso8Bqef3KR\ndY2Q1mtrfWyrX3uft+8QKeHwrsy6Wm63HLrc/jbwRN21Of1VFhuy9rFIhgqCIKgwVViuCYIgCEqI\nIB8EQVBhIsgHQRBUmAjyQRAEFSaCfBAEQYWJIB8EQVBhIsgHQRBUmAjyQRAEFeZfMJyPDK6TqsgA\nAAAASUVORK5CYII=\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plt.plot(time_vector, ref_result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's move on and port this synthesis function to Cython. We will then time the execution speed and see if Cython allows us to make this function faster.\n", "\n", "But first, let's make a detour. To port our function to Cython, we need to be able to do two things:\n", "\n", "- use complex number types in Cython\n", "- use the complex exponential \"from the C/C++ world\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Prerequesites for Cython & complex numbers " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Declaring complex numbers in Cython" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So how do you declare a complex number in Cython? Luckily, the Cython tutorial from Scipy 2017 by Kurt Smith comes to the rescue (specifically, [this notebook](https://github.com/kwmsmith/scipy-2017-cython-tutorial/blob/master/03-cython-types.ipynb), but check out [the whole tutorial](https://github.com/kwmsmith/scipy-2017-cython-tutorial), it's a great learning resource)." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "%load_ext cython" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(1+1j) (1+1j) (1+1j)\n", "1.0 1.0 (1-1j)\n" ] }, { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_535820ea99a6bba3370e40ba9afac1e3.pyx\n", " \n", " \n", "\n", "\n", "

Generated by Cython 0.25.2

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
 01: 
\n", "
 02: # `double complex` is preferred for compatibility with Python's `complex` type:
\n", "
 03: # https://docs.python.org/3/c-api/complex.html
\n", "
 04: 
\n", "
 05: cdef:
\n", "
+06:     float complex fc = 1+1j
\n", "
  __pyx_t_1 = __Pyx_c_sum_double(__pyx_t_double_complex_from_parts(1, 0), __pyx_t_double_complex_from_parts(0, 1.0));\n",
       "  __pyx_v_46_cython_magic_535820ea99a6bba3370e40ba9afac1e3_fc = __pyx_t_float_complex_from_parts(__Pyx_CREAL(__pyx_t_1), __Pyx_CIMAG(__pyx_t_1));\n",
       "
+07:     double complex dc = 1+1j
\n", "
  __pyx_v_46_cython_magic_535820ea99a6bba3370e40ba9afac1e3_dc = __Pyx_c_sum_double(__pyx_t_double_complex_from_parts(1, 0), __pyx_t_double_complex_from_parts(0, 1.0));\n",
       "
+08:     long double complex ldc = 1+1j
\n", "
  __pyx_t_1 = __Pyx_c_sum_double(__pyx_t_double_complex_from_parts(1, 0), __pyx_t_double_complex_from_parts(0, 1.0));\n",
       "  __pyx_v_46_cython_magic_535820ea99a6bba3370e40ba9afac1e3_ldc = __pyx_t_long_double_complex_from_parts(__Pyx_CREAL(__pyx_t_1), __Pyx_CIMAG(__pyx_t_1));\n",
       "
 09: 
\n", "
+10: print(fc, dc, ldc)
\n", "
  __pyx_t_2 = __pyx_PyComplex_FromComplex(__pyx_v_46_cython_magic_535820ea99a6bba3370e40ba9afac1e3_fc); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_2);\n",
       "  __pyx_t_3 = __pyx_PyComplex_FromComplex(__pyx_v_46_cython_magic_535820ea99a6bba3370e40ba9afac1e3_dc); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __pyx_t_4 = __pyx_PyComplex_FromComplex(__pyx_v_46_cython_magic_535820ea99a6bba3370e40ba9afac1e3_ldc); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __pyx_t_5 = PyTuple_New(3); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_GIVEREF(__pyx_t_2);\n",
       "  PyTuple_SET_ITEM(__pyx_t_5, 0, __pyx_t_2);\n",
       "  __Pyx_GIVEREF(__pyx_t_3);\n",
       "  PyTuple_SET_ITEM(__pyx_t_5, 1, __pyx_t_3);\n",
       "  __Pyx_GIVEREF(__pyx_t_4);\n",
       "  PyTuple_SET_ITEM(__pyx_t_5, 2, __pyx_t_4);\n",
       "  __pyx_t_2 = 0;\n",
       "  __pyx_t_3 = 0;\n",
       "  __pyx_t_4 = 0;\n",
       "  __pyx_t_4 = __Pyx_PyObject_Call(__pyx_builtin_print, __pyx_t_5, NULL); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "
+11: print(fc.real, dc.imag, ldc.conjugate())
\n", "
  __pyx_t_4 = PyFloat_FromDouble(__Pyx_CREAL(__pyx_v_46_cython_magic_535820ea99a6bba3370e40ba9afac1e3_fc)); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 11, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __pyx_t_5 = PyFloat_FromDouble(__Pyx_CIMAG(__pyx_v_46_cython_magic_535820ea99a6bba3370e40ba9afac1e3_dc)); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 11, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __pyx_t_6 = __Pyx_c_conj_long__double(__pyx_v_46_cython_magic_535820ea99a6bba3370e40ba9afac1e3_ldc);\n",
       "  __pyx_t_3 = __pyx_PyComplex_FromComplex(__pyx_t_6); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 11, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __pyx_t_2 = PyTuple_New(3); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 11, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_2);\n",
       "  __Pyx_GIVEREF(__pyx_t_4);\n",
       "  PyTuple_SET_ITEM(__pyx_t_2, 0, __pyx_t_4);\n",
       "  __Pyx_GIVEREF(__pyx_t_5);\n",
       "  PyTuple_SET_ITEM(__pyx_t_2, 1, __pyx_t_5);\n",
       "  __Pyx_GIVEREF(__pyx_t_3);\n",
       "  PyTuple_SET_ITEM(__pyx_t_2, 2, __pyx_t_3);\n",
       "  __pyx_t_4 = 0;\n",
       "  __pyx_t_5 = 0;\n",
       "  __pyx_t_3 = 0;\n",
       "  __pyx_t_3 = __Pyx_PyObject_Call(__pyx_builtin_print, __pyx_t_2, NULL); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 11, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a\n", "\n", "# `double complex` is preferred for compatibility with Python's `complex` type:\n", "# https://docs.python.org/3/c-api/complex.html\n", "\n", "cdef:\n", " float complex fc = 1+1j\n", " double complex dc = 1+1j\n", " long double complex ldc = 1+1j\n", "\n", "print(fc, dc, ldc)\n", "print(fc.real, dc.imag, ldc.conjugate())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Okay, but what about complex numbers in a NumPy array? Well, we can use the memoryview syntax for this." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_100be49ff458200c629e7c893dc56a0d.pyx\n", " \n", " \n", "\n", "\n", "

Generated by Cython 0.25.2

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
 1: 
\n", "
+2: def identity(complex [::1] weights):
\n", "
/* Python wrapper */\n",
       "static PyObject *__pyx_pw_46_cython_magic_100be49ff458200c629e7c893dc56a0d_1identity(PyObject *__pyx_self, PyObject *__pyx_arg_weights); /*proto*/\n",
       "static PyMethodDef __pyx_mdef_46_cython_magic_100be49ff458200c629e7c893dc56a0d_1identity = {\"identity\", (PyCFunction)__pyx_pw_46_cython_magic_100be49ff458200c629e7c893dc56a0d_1identity, METH_O, 0};\n",
       "static PyObject *__pyx_pw_46_cython_magic_100be49ff458200c629e7c893dc56a0d_1identity(PyObject *__pyx_self, PyObject *__pyx_arg_weights) {\n",
       "  __Pyx_memviewslice __pyx_v_weights = { 0, 0, { 0 }, { 0 }, { 0 } };\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"identity (wrapper)\", 0);\n",
       "  assert(__pyx_arg_weights); {\n",
       "    __pyx_v_weights = __Pyx_PyObject_to_MemoryviewSlice_dc___pyx_t_double_complex(__pyx_arg_weights); if (unlikely(!__pyx_v_weights.memview)) __PYX_ERR(0, 2, __pyx_L3_error)\n",
       "  }\n",
       "  goto __pyx_L4_argument_unpacking_done;\n",
       "  __pyx_L3_error:;\n",
       "  __Pyx_AddTraceback(\"_cython_magic_100be49ff458200c629e7c893dc56a0d.identity\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return NULL;\n",
       "  __pyx_L4_argument_unpacking_done:;\n",
       "  __pyx_r = __pyx_pf_46_cython_magic_100be49ff458200c629e7c893dc56a0d_identity(__pyx_self, __pyx_v_weights);\n",
       "\n",
       "  /* function exit code */\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_46_cython_magic_100be49ff458200c629e7c893dc56a0d_identity(CYTHON_UNUSED PyObject *__pyx_self, __Pyx_memviewslice __pyx_v_weights) {\n",
       "  PyObject *__pyx_r = NULL;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"identity\", 0);\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_L1_error:;\n",
       "  __Pyx_XDECREF(__pyx_t_1);\n",
       "  __Pyx_AddTraceback(\"_cython_magic_100be49ff458200c629e7c893dc56a0d.identity\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __PYX_XDEC_MEMVIEW(&__pyx_v_weights, 1);\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "/* … */\n",
       "  __pyx_tuple__14 = PyTuple_Pack(2, __pyx_n_s_weights, __pyx_n_s_weights); if (unlikely(!__pyx_tuple__14)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_tuple__14);\n",
       "  __Pyx_GIVEREF(__pyx_tuple__14);\n",
       "/* … */\n",
       "  __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_46_cython_magic_100be49ff458200c629e7c893dc56a0d_1identity, NULL, __pyx_n_s_cython_magic_100be49ff458200c62); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_identity, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __pyx_codeobj__15 = (PyObject*)__Pyx_PyCode_New(1, 0, 2, 0, 0, __pyx_empty_bytes, __pyx_empty_tuple, __pyx_empty_tuple, __pyx_tuple__14, __pyx_empty_tuple, __pyx_empty_tuple, __pyx_kp_s_C_Users_FL232714_ipython_cython, __pyx_n_s_identity, 2, __pyx_empty_bytes); if (unlikely(!__pyx_codeobj__15)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "
+3:     return weights
\n", "
  __Pyx_XDECREF(__pyx_r);\n",
       "  __pyx_t_1 = __pyx_memoryview_fromslice(__pyx_v_weights, 1, (PyObject *(*)(char *)) __pyx_memview_get___pyx_t_double_complex, (int (*)(char *, PyObject *)) __pyx_memview_set___pyx_t_double_complex, 0);; if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 3, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __pyx_r = __pyx_t_1;\n",
       "  __pyx_t_1 = 0;\n",
       "  goto __pyx_L0;\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a\n", "\n", "def identity(complex [::1] weights):\n", " return weights" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.allclose(weights, identity(weights))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But we can also use the ndarray syntax:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_d549c2f878d7aebcfe68c88542420167.pyx\n", " \n", " \n", "\n", "\n", "

Generated by Cython 0.25.2

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
+1: cimport numpy as np
\n", "
  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 2: 
\n", "
+3: def identity(np.ndarray [np.complex128_t, ndim=1] weights):
\n", "
/* Python wrapper */\n",
       "static PyObject *__pyx_pw_46_cython_magic_d549c2f878d7aebcfe68c88542420167_1identity(PyObject *__pyx_self, PyObject *__pyx_v_weights); /*proto*/\n",
       "static PyMethodDef __pyx_mdef_46_cython_magic_d549c2f878d7aebcfe68c88542420167_1identity = {\"identity\", (PyCFunction)__pyx_pw_46_cython_magic_d549c2f878d7aebcfe68c88542420167_1identity, METH_O, 0};\n",
       "static PyObject *__pyx_pw_46_cython_magic_d549c2f878d7aebcfe68c88542420167_1identity(PyObject *__pyx_self, PyObject *__pyx_v_weights) {\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"identity (wrapper)\", 0);\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_weights), __pyx_ptype_5numpy_ndarray, 1, \"weights\", 0))) __PYX_ERR(0, 3, __pyx_L1_error)\n",
       "  __pyx_r = __pyx_pf_46_cython_magic_d549c2f878d7aebcfe68c88542420167_identity(__pyx_self, ((PyArrayObject *)__pyx_v_weights));\n",
       "\n",
       "  /* function exit code */\n",
       "  goto __pyx_L0;\n",
       "  __pyx_L1_error:;\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_46_cython_magic_d549c2f878d7aebcfe68c88542420167_identity(CYTHON_UNUSED PyObject *__pyx_self, PyArrayObject *__pyx_v_weights) {\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_weights;\n",
       "  __Pyx_Buffer __pyx_pybuffer_weights;\n",
       "  PyObject *__pyx_r = NULL;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"identity\", 0);\n",
       "  __pyx_pybuffer_weights.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_weights.refcount = 0;\n",
       "  __pyx_pybuffernd_weights.data = NULL;\n",
       "  __pyx_pybuffernd_weights.rcbuffer = &__pyx_pybuffer_weights;\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_weights.rcbuffer->pybuffer, (PyObject*)__pyx_v_weights, &__Pyx_TypeInfo___pyx_t_double_complex, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 3, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_weights.diminfo[0].strides = __pyx_pybuffernd_weights.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_weights.diminfo[0].shape = __pyx_pybuffernd_weights.rcbuffer->pybuffer.shape[0];\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_L1_error:;\n",
       "  { PyObject *__pyx_type, *__pyx_value, *__pyx_tb;\n",
       "    __Pyx_PyThreadState_declare\n",
       "    __Pyx_PyThreadState_assign\n",
       "    __Pyx_ErrFetch(&__pyx_type, &__pyx_value, &__pyx_tb);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __Pyx_ErrRestore(__pyx_type, __pyx_value, __pyx_tb);}\n",
       "  __Pyx_AddTraceback(\"_cython_magic_d549c2f878d7aebcfe68c88542420167.identity\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  goto __pyx_L2;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __pyx_L2:;\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "/* … */\n",
       "  __pyx_tuple__10 = PyTuple_Pack(1, __pyx_n_s_weights); if (unlikely(!__pyx_tuple__10)) __PYX_ERR(0, 3, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_tuple__10);\n",
       "  __Pyx_GIVEREF(__pyx_tuple__10);\n",
       "/* … */\n",
       "  __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_46_cython_magic_d549c2f878d7aebcfe68c88542420167_1identity, NULL, __pyx_n_s_cython_magic_d549c2f878d7aebcfe); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 3, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_identity, __pyx_t_1) < 0) __PYX_ERR(0, 3, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
+4:     return weights
\n", "
  __Pyx_XDECREF(__pyx_r);\n",
       "  __Pyx_INCREF(((PyObject *)__pyx_v_weights));\n",
       "  __pyx_r = ((PyObject *)__pyx_v_weights);\n",
       "  goto __pyx_L0;\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a\n", "cimport numpy as np\n", "\n", "def identity(np.ndarray [np.complex128_t, ndim=1] weights):\n", " return weights" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.allclose(weights, identity(weights))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will stick to the above syntax (*np.ndarray*) in what follows." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The complex exponential" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Another thing we need is to have access to the complex exponential in the Cython code. A naive approach is to use the NumPy exponetial: " ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_e6c0f8824314565e8d5c73d53395537a.pyx\n", " \n", " \n", "\n", "\n", "

Generated by Cython 0.25.2

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
+01: cimport numpy as np
\n", "
  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
+02: import numpy as np
\n", "
  __pyx_t_1 = __Pyx_Import(__pyx_n_s_numpy, 0, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_np, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 03: import cython
\n", "
 04: 
\n", "
 05: @cython.boundscheck(False)
\n", "
 06: @cython.wraparound(False)
\n", "
+07: def apply_complex_exp(np.ndarray [np.complex128_t, ndim=1] weights, np.ndarray [np.complex128_t, ndim=1] out):
\n", "
/* Python wrapper */\n",
       "static PyObject *__pyx_pw_46_cython_magic_e6c0f8824314565e8d5c73d53395537a_1apply_complex_exp(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/\n",
       "static PyMethodDef __pyx_mdef_46_cython_magic_e6c0f8824314565e8d5c73d53395537a_1apply_complex_exp = {\"apply_complex_exp\", (PyCFunction)__pyx_pw_46_cython_magic_e6c0f8824314565e8d5c73d53395537a_1apply_complex_exp, METH_VARARGS|METH_KEYWORDS, 0};\n",
       "static PyObject *__pyx_pw_46_cython_magic_e6c0f8824314565e8d5c73d53395537a_1apply_complex_exp(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {\n",
       "  PyArrayObject *__pyx_v_weights = 0;\n",
       "  PyArrayObject *__pyx_v_out = 0;\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"apply_complex_exp (wrapper)\", 0);\n",
       "  {\n",
       "    static PyObject **__pyx_pyargnames[] = {&__pyx_n_s_weights,&__pyx_n_s_out,0};\n",
       "    PyObject* values[2] = {0,0};\n",
       "    if (unlikely(__pyx_kwds)) {\n",
       "      Py_ssize_t kw_args;\n",
       "      const Py_ssize_t pos_args = PyTuple_GET_SIZE(__pyx_args);\n",
       "      switch (pos_args) {\n",
       "        case  2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "        case  1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "        case  0: break;\n",
       "        default: goto __pyx_L5_argtuple_error;\n",
       "      }\n",
       "      kw_args = PyDict_Size(__pyx_kwds);\n",
       "      switch (pos_args) {\n",
       "        case  0:\n",
       "        if (likely((values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_weights)) != 0)) kw_args--;\n",
       "        else goto __pyx_L5_argtuple_error;\n",
       "        case  1:\n",
       "        if (likely((values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_out)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"apply_complex_exp\", 1, 2, 2, 1); __PYX_ERR(0, 7, __pyx_L3_error)\n",
       "        }\n",
       "      }\n",
       "      if (unlikely(kw_args > 0)) {\n",
       "        if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, pos_args, \"apply_complex_exp\") < 0)) __PYX_ERR(0, 7, __pyx_L3_error)\n",
       "      }\n",
       "    } else if (PyTuple_GET_SIZE(__pyx_args) != 2) {\n",
       "      goto __pyx_L5_argtuple_error;\n",
       "    } else {\n",
       "      values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "      values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "    }\n",
       "    __pyx_v_weights = ((PyArrayObject *)values[0]);\n",
       "    __pyx_v_out = ((PyArrayObject *)values[1]);\n",
       "  }\n",
       "  goto __pyx_L4_argument_unpacking_done;\n",
       "  __pyx_L5_argtuple_error:;\n",
       "  __Pyx_RaiseArgtupleInvalid(\"apply_complex_exp\", 1, 2, 2, PyTuple_GET_SIZE(__pyx_args)); __PYX_ERR(0, 7, __pyx_L3_error)\n",
       "  __pyx_L3_error:;\n",
       "  __Pyx_AddTraceback(\"_cython_magic_e6c0f8824314565e8d5c73d53395537a.apply_complex_exp\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return NULL;\n",
       "  __pyx_L4_argument_unpacking_done:;\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_weights), __pyx_ptype_5numpy_ndarray, 1, \"weights\", 0))) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_out), __pyx_ptype_5numpy_ndarray, 1, \"out\", 0))) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  __pyx_r = __pyx_pf_46_cython_magic_e6c0f8824314565e8d5c73d53395537a_apply_complex_exp(__pyx_self, __pyx_v_weights, __pyx_v_out);\n",
       "\n",
       "  /* function exit code */\n",
       "  goto __pyx_L0;\n",
       "  __pyx_L1_error:;\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_46_cython_magic_e6c0f8824314565e8d5c73d53395537a_apply_complex_exp(CYTHON_UNUSED PyObject *__pyx_self, PyArrayObject *__pyx_v_weights, PyArrayObject *__pyx_v_out) {\n",
       "  int __pyx_v_i;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_out;\n",
       "  __Pyx_Buffer __pyx_pybuffer_out;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_weights;\n",
       "  __Pyx_Buffer __pyx_pybuffer_weights;\n",
       "  PyObject *__pyx_r = NULL;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"apply_complex_exp\", 0);\n",
       "  __pyx_pybuffer_weights.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_weights.refcount = 0;\n",
       "  __pyx_pybuffernd_weights.data = NULL;\n",
       "  __pyx_pybuffernd_weights.rcbuffer = &__pyx_pybuffer_weights;\n",
       "  __pyx_pybuffer_out.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_out.refcount = 0;\n",
       "  __pyx_pybuffernd_out.data = NULL;\n",
       "  __pyx_pybuffernd_out.rcbuffer = &__pyx_pybuffer_out;\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_weights.rcbuffer->pybuffer, (PyObject*)__pyx_v_weights, &__Pyx_TypeInfo___pyx_t_double_complex, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_weights.diminfo[0].strides = __pyx_pybuffernd_weights.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_weights.diminfo[0].shape = __pyx_pybuffernd_weights.rcbuffer->pybuffer.shape[0];\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_out.rcbuffer->pybuffer, (PyObject*)__pyx_v_out, &__Pyx_TypeInfo___pyx_t_double_complex, PyBUF_FORMAT| PyBUF_STRIDES| PyBUF_WRITABLE, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_out.diminfo[0].strides = __pyx_pybuffernd_out.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_out.diminfo[0].shape = __pyx_pybuffernd_out.rcbuffer->pybuffer.shape[0];\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_r = Py_None; __Pyx_INCREF(Py_None);\n",
       "  goto __pyx_L0;\n",
       "  __pyx_L1_error:;\n",
       "  __Pyx_XDECREF(__pyx_t_3);\n",
       "  __Pyx_XDECREF(__pyx_t_4);\n",
       "  __Pyx_XDECREF(__pyx_t_5);\n",
       "  __Pyx_XDECREF(__pyx_t_8);\n",
       "  __Pyx_XDECREF(__pyx_t_9);\n",
       "  { PyObject *__pyx_type, *__pyx_value, *__pyx_tb;\n",
       "    __Pyx_PyThreadState_declare\n",
       "    __Pyx_PyThreadState_assign\n",
       "    __Pyx_ErrFetch(&__pyx_type, &__pyx_value, &__pyx_tb);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __Pyx_ErrRestore(__pyx_type, __pyx_value, __pyx_tb);}\n",
       "  __Pyx_AddTraceback(\"_cython_magic_e6c0f8824314565e8d5c73d53395537a.apply_complex_exp\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  goto __pyx_L2;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __pyx_L2:;\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "/* … */\n",
       "  __pyx_tuple__10 = PyTuple_Pack(3, __pyx_n_s_weights, __pyx_n_s_out, __pyx_n_s_i); if (unlikely(!__pyx_tuple__10)) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_tuple__10);\n",
       "  __Pyx_GIVEREF(__pyx_tuple__10);\n",
       "/* … */\n",
       "  __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_46_cython_magic_e6c0f8824314565e8d5c73d53395537a_1apply_complex_exp, NULL, __pyx_n_s_cython_magic_e6c0f8824314565e8d); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_apply_complex_exp, __pyx_t_1) < 0) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 08:     cdef int i
\n", "
+09:     for i in range(weights.shape[0]):
\n", "
  __pyx_t_1 = (__pyx_v_weights->dimensions[0]);\n",
       "  for (__pyx_t_2 = 0; __pyx_t_2 < __pyx_t_1; __pyx_t_2+=1) {\n",
       "    __pyx_v_i = __pyx_t_2;\n",
       "
+10:         out[i] = np.exp(1j * weights[i])
\n", "
    __pyx_t_4 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "    __Pyx_GOTREF(__pyx_t_4);\n",
       "    __pyx_t_5 = __Pyx_PyObject_GetAttrStr(__pyx_t_4, __pyx_n_s_exp); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "    __Pyx_GOTREF(__pyx_t_5);\n",
       "    __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "    __pyx_t_6 = __pyx_v_i;\n",
       "    __pyx_t_7 = __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(0, 1.0), (*__Pyx_BufPtrStrided1d(__pyx_t_double_complex *, __pyx_pybuffernd_weights.rcbuffer->pybuffer.buf, __pyx_t_6, __pyx_pybuffernd_weights.diminfo[0].strides)));\n",
       "    __pyx_t_4 = __pyx_PyComplex_FromComplex(__pyx_t_7); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "    __Pyx_GOTREF(__pyx_t_4);\n",
       "    __pyx_t_8 = NULL;\n",
       "    if (CYTHON_UNPACK_METHODS && unlikely(PyMethod_Check(__pyx_t_5))) {\n",
       "      __pyx_t_8 = PyMethod_GET_SELF(__pyx_t_5);\n",
       "      if (likely(__pyx_t_8)) {\n",
       "        PyObject* function = PyMethod_GET_FUNCTION(__pyx_t_5);\n",
       "        __Pyx_INCREF(__pyx_t_8);\n",
       "        __Pyx_INCREF(function);\n",
       "        __Pyx_DECREF_SET(__pyx_t_5, function);\n",
       "      }\n",
       "    }\n",
       "    if (!__pyx_t_8) {\n",
       "      __pyx_t_3 = __Pyx_PyObject_CallOneArg(__pyx_t_5, __pyx_t_4); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "      __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "      __Pyx_GOTREF(__pyx_t_3);\n",
       "    } else {\n",
       "      #if CYTHON_FAST_PYCALL\n",
       "      if (PyFunction_Check(__pyx_t_5)) {\n",
       "        PyObject *__pyx_temp[2] = {__pyx_t_8, __pyx_t_4};\n",
       "        __pyx_t_3 = __Pyx_PyFunction_FastCall(__pyx_t_5, __pyx_temp+1-1, 1+1); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "        __Pyx_XDECREF(__pyx_t_8); __pyx_t_8 = 0;\n",
       "        __Pyx_GOTREF(__pyx_t_3);\n",
       "        __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "      } else\n",
       "      #endif\n",
       "      #if CYTHON_FAST_PYCCALL\n",
       "      if (__Pyx_PyFastCFunction_Check(__pyx_t_5)) {\n",
       "        PyObject *__pyx_temp[2] = {__pyx_t_8, __pyx_t_4};\n",
       "        __pyx_t_3 = __Pyx_PyCFunction_FastCall(__pyx_t_5, __pyx_temp+1-1, 1+1); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "        __Pyx_XDECREF(__pyx_t_8); __pyx_t_8 = 0;\n",
       "        __Pyx_GOTREF(__pyx_t_3);\n",
       "        __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "      } else\n",
       "      #endif\n",
       "      {\n",
       "        __pyx_t_9 = PyTuple_New(1+1); if (unlikely(!__pyx_t_9)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "        __Pyx_GOTREF(__pyx_t_9);\n",
       "        __Pyx_GIVEREF(__pyx_t_8); PyTuple_SET_ITEM(__pyx_t_9, 0, __pyx_t_8); __pyx_t_8 = NULL;\n",
       "        __Pyx_GIVEREF(__pyx_t_4);\n",
       "        PyTuple_SET_ITEM(__pyx_t_9, 0+1, __pyx_t_4);\n",
       "        __pyx_t_4 = 0;\n",
       "        __pyx_t_3 = __Pyx_PyObject_Call(__pyx_t_5, __pyx_t_9, NULL); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "        __Pyx_GOTREF(__pyx_t_3);\n",
       "        __Pyx_DECREF(__pyx_t_9); __pyx_t_9 = 0;\n",
       "      }\n",
       "    }\n",
       "    __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;\n",
       "    __pyx_t_7 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_3); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "    __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;\n",
       "    __pyx_t_10 = __pyx_v_i;\n",
       "    *__Pyx_BufPtrStrided1d(__pyx_t_double_complex *, __pyx_pybuffernd_out.rcbuffer->pybuffer.buf, __pyx_t_10, __pyx_pybuffernd_out.diminfo[0].strides) = __pyx_t_7;\n",
       "  }\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a\n", "cimport numpy as np\n", "import numpy as np\n", "import cython\n", "\n", "@cython.boundscheck(False)\n", "@cython.wraparound(False)\n", "def apply_complex_exp(np.ndarray [np.complex128_t, ndim=1] weights, np.ndarray [np.complex128_t, ndim=1] out):\n", " cdef int i\n", " for i in range(weights.shape[0]):\n", " out[i] = np.exp(1j * weights[i])" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": true }, "outputs": [], "source": [ "out = np.empty_like(weights)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dtype('complex128')" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "out.dtype" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "apply_complex_exp(weights, out)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's check the result:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.allclose(np.exp(1j * weights), out)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, the problem is that we don't get pure C speed on this operation due to the call to a numpy function. There must be a better way. Searching the internet, I found [this StackOverflow thread](https://stackoverflow.com/questions/27906862/complex-valued-calculations-using-cython), which suggests to do the following." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_d867f5e67c8f54a1604814be92f853ab.pyx\n", " \n", " \n", "\n", "\n", "

Generated by Cython 0.25.2

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
+01: cimport numpy as np
\n", "
  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
+02: import numpy as np
\n", "
  __pyx_t_1 = __Pyx_Import(__pyx_n_s_numpy, 0, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_np, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 03: import cython
\n", "
 04: 
\n", "
 05: cdef extern from "complex.h":
\n", "
 06:     double complex exp(double complex)
\n", "
 07: 
\n", "
 08: @cython.boundscheck(False)
\n", "
 09: @cython.wraparound(False)
\n", "
+10: def apply_complex_exp2(np.ndarray [np.complex128_t, ndim=1] weights, np.ndarray [np.complex128_t, ndim=1] out):
\n", "
/* Python wrapper */\n",
       "static PyObject *__pyx_pw_46_cython_magic_d867f5e67c8f54a1604814be92f853ab_1apply_complex_exp2(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/\n",
       "static PyMethodDef __pyx_mdef_46_cython_magic_d867f5e67c8f54a1604814be92f853ab_1apply_complex_exp2 = {\"apply_complex_exp2\", (PyCFunction)__pyx_pw_46_cython_magic_d867f5e67c8f54a1604814be92f853ab_1apply_complex_exp2, METH_VARARGS|METH_KEYWORDS, 0};\n",
       "static PyObject *__pyx_pw_46_cython_magic_d867f5e67c8f54a1604814be92f853ab_1apply_complex_exp2(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {\n",
       "  PyArrayObject *__pyx_v_weights = 0;\n",
       "  PyArrayObject *__pyx_v_out = 0;\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"apply_complex_exp2 (wrapper)\", 0);\n",
       "  {\n",
       "    static PyObject **__pyx_pyargnames[] = {&__pyx_n_s_weights,&__pyx_n_s_out,0};\n",
       "    PyObject* values[2] = {0,0};\n",
       "    if (unlikely(__pyx_kwds)) {\n",
       "      Py_ssize_t kw_args;\n",
       "      const Py_ssize_t pos_args = PyTuple_GET_SIZE(__pyx_args);\n",
       "      switch (pos_args) {\n",
       "        case  2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "        case  1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "        case  0: break;\n",
       "        default: goto __pyx_L5_argtuple_error;\n",
       "      }\n",
       "      kw_args = PyDict_Size(__pyx_kwds);\n",
       "      switch (pos_args) {\n",
       "        case  0:\n",
       "        if (likely((values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_weights)) != 0)) kw_args--;\n",
       "        else goto __pyx_L5_argtuple_error;\n",
       "        case  1:\n",
       "        if (likely((values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_out)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"apply_complex_exp2\", 1, 2, 2, 1); __PYX_ERR(0, 10, __pyx_L3_error)\n",
       "        }\n",
       "      }\n",
       "      if (unlikely(kw_args > 0)) {\n",
       "        if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, pos_args, \"apply_complex_exp2\") < 0)) __PYX_ERR(0, 10, __pyx_L3_error)\n",
       "      }\n",
       "    } else if (PyTuple_GET_SIZE(__pyx_args) != 2) {\n",
       "      goto __pyx_L5_argtuple_error;\n",
       "    } else {\n",
       "      values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "      values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "    }\n",
       "    __pyx_v_weights = ((PyArrayObject *)values[0]);\n",
       "    __pyx_v_out = ((PyArrayObject *)values[1]);\n",
       "  }\n",
       "  goto __pyx_L4_argument_unpacking_done;\n",
       "  __pyx_L5_argtuple_error:;\n",
       "  __Pyx_RaiseArgtupleInvalid(\"apply_complex_exp2\", 1, 2, 2, PyTuple_GET_SIZE(__pyx_args)); __PYX_ERR(0, 10, __pyx_L3_error)\n",
       "  __pyx_L3_error:;\n",
       "  __Pyx_AddTraceback(\"_cython_magic_d867f5e67c8f54a1604814be92f853ab.apply_complex_exp2\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return NULL;\n",
       "  __pyx_L4_argument_unpacking_done:;\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_weights), __pyx_ptype_5numpy_ndarray, 1, \"weights\", 0))) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_out), __pyx_ptype_5numpy_ndarray, 1, \"out\", 0))) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  __pyx_r = __pyx_pf_46_cython_magic_d867f5e67c8f54a1604814be92f853ab_apply_complex_exp2(__pyx_self, __pyx_v_weights, __pyx_v_out);\n",
       "\n",
       "  /* function exit code */\n",
       "  goto __pyx_L0;\n",
       "  __pyx_L1_error:;\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_46_cython_magic_d867f5e67c8f54a1604814be92f853ab_apply_complex_exp2(CYTHON_UNUSED PyObject *__pyx_self, PyArrayObject *__pyx_v_weights, PyArrayObject *__pyx_v_out) {\n",
       "  int __pyx_v_i;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_out;\n",
       "  __Pyx_Buffer __pyx_pybuffer_out;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_weights;\n",
       "  __Pyx_Buffer __pyx_pybuffer_weights;\n",
       "  PyObject *__pyx_r = NULL;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"apply_complex_exp2\", 0);\n",
       "  __pyx_pybuffer_weights.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_weights.refcount = 0;\n",
       "  __pyx_pybuffernd_weights.data = NULL;\n",
       "  __pyx_pybuffernd_weights.rcbuffer = &__pyx_pybuffer_weights;\n",
       "  __pyx_pybuffer_out.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_out.refcount = 0;\n",
       "  __pyx_pybuffernd_out.data = NULL;\n",
       "  __pyx_pybuffernd_out.rcbuffer = &__pyx_pybuffer_out;\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_weights.rcbuffer->pybuffer, (PyObject*)__pyx_v_weights, &__Pyx_TypeInfo___pyx_t_double_complex, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_weights.diminfo[0].strides = __pyx_pybuffernd_weights.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_weights.diminfo[0].shape = __pyx_pybuffernd_weights.rcbuffer->pybuffer.shape[0];\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_out.rcbuffer->pybuffer, (PyObject*)__pyx_v_out, &__Pyx_TypeInfo___pyx_t_double_complex, PyBUF_FORMAT| PyBUF_STRIDES| PyBUF_WRITABLE, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_out.diminfo[0].strides = __pyx_pybuffernd_out.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_out.diminfo[0].shape = __pyx_pybuffernd_out.rcbuffer->pybuffer.shape[0];\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_r = Py_None; __Pyx_INCREF(Py_None);\n",
       "  goto __pyx_L0;\n",
       "  __pyx_L1_error:;\n",
       "  { PyObject *__pyx_type, *__pyx_value, *__pyx_tb;\n",
       "    __Pyx_PyThreadState_declare\n",
       "    __Pyx_PyThreadState_assign\n",
       "    __Pyx_ErrFetch(&__pyx_type, &__pyx_value, &__pyx_tb);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __Pyx_ErrRestore(__pyx_type, __pyx_value, __pyx_tb);}\n",
       "  __Pyx_AddTraceback(\"_cython_magic_d867f5e67c8f54a1604814be92f853ab.apply_complex_exp2\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  goto __pyx_L2;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __pyx_L2:;\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "/* … */\n",
       "  __pyx_tuple__10 = PyTuple_Pack(3, __pyx_n_s_weights, __pyx_n_s_out, __pyx_n_s_i); if (unlikely(!__pyx_tuple__10)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_tuple__10);\n",
       "  __Pyx_GIVEREF(__pyx_tuple__10);\n",
       "/* … */\n",
       "  __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_46_cython_magic_d867f5e67c8f54a1604814be92f853ab_1apply_complex_exp2, NULL, __pyx_n_s_cython_magic_d867f5e67c8f54a160); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_apply_complex_exp2, __pyx_t_1) < 0) __PYX_ERR(0, 10, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 11:     cdef int i
\n", "
+12:     for i in range(weights.shape[0]):
\n", "
  __pyx_t_1 = (__pyx_v_weights->dimensions[0]);\n",
       "  for (__pyx_t_2 = 0; __pyx_t_2 < __pyx_t_1; __pyx_t_2+=1) {\n",
       "    __pyx_v_i = __pyx_t_2;\n",
       "
+13:         out[i] = exp(1j * weights[i])
\n", "
    __pyx_t_3 = __pyx_v_i;\n",
       "    __pyx_t_4 = __pyx_v_i;\n",
       "    *__Pyx_BufPtrStrided1d(__pyx_t_double_complex *, __pyx_pybuffernd_out.rcbuffer->pybuffer.buf, __pyx_t_4, __pyx_pybuffernd_out.diminfo[0].strides) = exp(__Pyx_c_prod_double(__pyx_t_double_complex_from_parts(0, 1.0), (*__Pyx_BufPtrStrided1d(__pyx_t_double_complex *, __pyx_pybuffernd_weights.rcbuffer->pybuffer.buf, __pyx_t_3, __pyx_pybuffernd_weights.diminfo[0].strides))));\n",
       "  }\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a --cplus\n", "cimport numpy as np\n", "import numpy as np\n", "import cython\n", "\n", "cdef extern from \"complex.h\":\n", " double complex exp(double complex)\n", "\n", "@cython.boundscheck(False)\n", "@cython.wraparound(False)\n", "def apply_complex_exp2(np.ndarray [np.complex128_t, ndim=1] weights, np.ndarray [np.complex128_t, ndim=1] out):\n", " cdef int i\n", " for i in range(weights.shape[0]):\n", " out[i] = exp(1j * weights[i])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This allows us translating to pure C... wait-for-it... ++! If you look closely at the above code cell, I had to add the `--cplus` flag to the compiler since the extern definition we are referring to is the built-in header for the a file that on my computer can be found in `Anaconda\\Lib\\site-packages\\Cython\\Includes\\libcpp`! If I don't add this, I get an error from the compiler..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's check that this returns the right result." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "apply_complex_exp2(weights, out)" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.allclose(np.exp(1j * weights), out)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's also check that this is faster:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.24 ms ± 100 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" ] } ], "source": [ "%timeit apply_complex_exp(weights, out)" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "40.8 µs ± 3.87 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n" ] } ], "source": [ "%timeit apply_complex_exp2(weights, out)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Indeed, the second approach that uses C++ is much faster!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With these things in mind, let's now write the Cython version of our reference implementation." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Cython optimization of the synthesis function " ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_180265c64445be1e3c6d6a7fae5a6faf.pyx\n", " \n", " \n", "\n", "\n", "

Generated by Cython 0.25.2

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
+01: cimport numpy as np
\n", "
  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
+02: import numpy as np
\n", "
  __pyx_t_1 = __Pyx_Import(__pyx_n_s_numpy, 0, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_np, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 03: import cython
\n", "
 04: 
\n", "
 05: cdef extern from "<complex.h>" namespace "std" nogil:
\n", "
 06:     double complex exp(double complex z)
\n", "
 07:     double real(double complex z)
\n", "
 08: 
\n", "
+09: cdef double complex I = 1j
\n", "
  __pyx_v_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_I = __pyx_t_double_complex_from_parts(0, 1.0);\n",
       "
 10: 
\n", "
 11: @cython.boundscheck(False)
\n", "
 12: @cython.wraparound(False)
\n", "
+13: def synthesis_cython1(np.ndarray [np.complex128_t, ndim=1] weights,
\n", "
/* Python wrapper */\n",
       "static PyObject *__pyx_pw_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_1synthesis_cython1(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/\n",
       "static PyMethodDef __pyx_mdef_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_1synthesis_cython1 = {\"synthesis_cython1\", (PyCFunction)__pyx_pw_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_1synthesis_cython1, METH_VARARGS|METH_KEYWORDS, 0};\n",
       "static PyObject *__pyx_pw_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_1synthesis_cython1(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {\n",
       "  PyArrayObject *__pyx_v_weights = 0;\n",
       "  double __pyx_v_omega;\n",
       "  PyArrayObject *__pyx_v_time_vector = 0;\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"synthesis_cython1 (wrapper)\", 0);\n",
       "  {\n",
       "    static PyObject **__pyx_pyargnames[] = {&__pyx_n_s_weights,&__pyx_n_s_omega,&__pyx_n_s_time_vector,0};\n",
       "    PyObject* values[3] = {0,0,0};\n",
       "    if (unlikely(__pyx_kwds)) {\n",
       "      Py_ssize_t kw_args;\n",
       "      const Py_ssize_t pos_args = PyTuple_GET_SIZE(__pyx_args);\n",
       "      switch (pos_args) {\n",
       "        case  3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);\n",
       "        case  2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "        case  1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "        case  0: break;\n",
       "        default: goto __pyx_L5_argtuple_error;\n",
       "      }\n",
       "      kw_args = PyDict_Size(__pyx_kwds);\n",
       "      switch (pos_args) {\n",
       "        case  0:\n",
       "        if (likely((values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_weights)) != 0)) kw_args--;\n",
       "        else goto __pyx_L5_argtuple_error;\n",
       "        case  1:\n",
       "        if (likely((values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_omega)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"synthesis_cython1\", 1, 3, 3, 1); __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "        }\n",
       "        case  2:\n",
       "        if (likely((values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_time_vector)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"synthesis_cython1\", 1, 3, 3, 2); __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "        }\n",
       "      }\n",
       "      if (unlikely(kw_args > 0)) {\n",
       "        if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, pos_args, \"synthesis_cython1\") < 0)) __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "      }\n",
       "    } else if (PyTuple_GET_SIZE(__pyx_args) != 3) {\n",
       "      goto __pyx_L5_argtuple_error;\n",
       "    } else {\n",
       "      values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "      values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "      values[2] = PyTuple_GET_ITEM(__pyx_args, 2);\n",
       "    }\n",
       "    __pyx_v_weights = ((PyArrayObject *)values[0]);\n",
       "    __pyx_v_omega = __pyx_PyFloat_AsDouble(values[1]); if (unlikely((__pyx_v_omega == (double)-1) && PyErr_Occurred())) __PYX_ERR(0, 14, __pyx_L3_error)\n",
       "    __pyx_v_time_vector = ((PyArrayObject *)values[2]);\n",
       "  }\n",
       "  goto __pyx_L4_argument_unpacking_done;\n",
       "  __pyx_L5_argtuple_error:;\n",
       "  __Pyx_RaiseArgtupleInvalid(\"synthesis_cython1\", 1, 3, 3, PyTuple_GET_SIZE(__pyx_args)); __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "  __pyx_L3_error:;\n",
       "  __Pyx_AddTraceback(\"_cython_magic_180265c64445be1e3c6d6a7fae5a6faf.synthesis_cython1\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return NULL;\n",
       "  __pyx_L4_argument_unpacking_done:;\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_weights), __pyx_ptype_5numpy_ndarray, 1, \"weights\", 0))) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_time_vector), __pyx_ptype_5numpy_ndarray, 1, \"time_vector\", 0))) __PYX_ERR(0, 15, __pyx_L1_error)\n",
       "  __pyx_r = __pyx_pf_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_synthesis_cython1(__pyx_self, __pyx_v_weights, __pyx_v_omega, __pyx_v_time_vector);\n",
       "\n",
       "  /* function exit code */\n",
       "  goto __pyx_L0;\n",
       "  __pyx_L1_error:;\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_synthesis_cython1(CYTHON_UNUSED PyObject *__pyx_self, PyArrayObject *__pyx_v_weights, double __pyx_v_omega, PyArrayObject *__pyx_v_time_vector) {\n",
       "  int __pyx_v_i;\n",
       "  int __pyx_v_j;\n",
       "  PyArrayObject *__pyx_v_out = 0;\n",
       "  __pyx_t_double_complex __pyx_v_temp_sum;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_out;\n",
       "  __Pyx_Buffer __pyx_pybuffer_out;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_time_vector;\n",
       "  __Pyx_Buffer __pyx_pybuffer_time_vector;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_weights;\n",
       "  __Pyx_Buffer __pyx_pybuffer_weights;\n",
       "  PyObject *__pyx_r = NULL;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"synthesis_cython1\", 0);\n",
       "  __pyx_pybuffer_out.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_out.refcount = 0;\n",
       "  __pyx_pybuffernd_out.data = NULL;\n",
       "  __pyx_pybuffernd_out.rcbuffer = &__pyx_pybuffer_out;\n",
       "  __pyx_pybuffer_weights.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_weights.refcount = 0;\n",
       "  __pyx_pybuffernd_weights.data = NULL;\n",
       "  __pyx_pybuffernd_weights.rcbuffer = &__pyx_pybuffer_weights;\n",
       "  __pyx_pybuffer_time_vector.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_time_vector.refcount = 0;\n",
       "  __pyx_pybuffernd_time_vector.data = NULL;\n",
       "  __pyx_pybuffernd_time_vector.rcbuffer = &__pyx_pybuffer_time_vector;\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_weights.rcbuffer->pybuffer, (PyObject*)__pyx_v_weights, &__Pyx_TypeInfo___pyx_t_double_complex, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_weights.diminfo[0].strides = __pyx_pybuffernd_weights.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_weights.diminfo[0].shape = __pyx_pybuffernd_weights.rcbuffer->pybuffer.shape[0];\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer, (PyObject*)__pyx_v_time_vector, &__Pyx_TypeInfo_nn___pyx_t_5numpy_float64_t, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_time_vector.diminfo[0].strides = __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_time_vector.diminfo[0].shape = __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.shape[0];\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_L1_error:;\n",
       "  __Pyx_XDECREF(__pyx_t_1);\n",
       "  __Pyx_XDECREF(__pyx_t_2);\n",
       "  __Pyx_XDECREF(__pyx_t_3);\n",
       "  __Pyx_XDECREF(__pyx_t_4);\n",
       "  __Pyx_XDECREF(__pyx_t_5);\n",
       "  { PyObject *__pyx_type, *__pyx_value, *__pyx_tb;\n",
       "    __Pyx_PyThreadState_declare\n",
       "    __Pyx_PyThreadState_assign\n",
       "    __Pyx_ErrFetch(&__pyx_type, &__pyx_value, &__pyx_tb);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __Pyx_ErrRestore(__pyx_type, __pyx_value, __pyx_tb);}\n",
       "  __Pyx_AddTraceback(\"_cython_magic_180265c64445be1e3c6d6a7fae5a6faf.synthesis_cython1\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  goto __pyx_L2;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __pyx_L2:;\n",
       "  __Pyx_XDECREF((PyObject *)__pyx_v_out);\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "/* … */\n",
       "  __pyx_tuple__10 = PyTuple_Pack(7, __pyx_n_s_weights, __pyx_n_s_omega, __pyx_n_s_time_vector, __pyx_n_s_i, __pyx_n_s_j, __pyx_n_s_out, __pyx_n_s_temp_sum); if (unlikely(!__pyx_tuple__10)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_tuple__10);\n",
       "  __Pyx_GIVEREF(__pyx_tuple__10);\n",
       "/* … */\n",
       "  __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_1synthesis_cython1, NULL, __pyx_n_s_cython_magic_180265c64445be1e3c); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_synthesis_cython1, __pyx_t_1) < 0) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 14:                      double omega,
\n", "
 15:                      np.ndarray [np.float64_t, ndim=1] time_vector):
\n", "
 16:     cdef int i, j
\n", "
+17:     cdef np.ndarray [np.float64_t, ndim=1] out = np.empty_like(time_vector, dtype=np.float)
\n", "
  __pyx_t_1 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __pyx_t_2 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_empty_like); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_2);\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __pyx_t_1 = PyTuple_New(1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __Pyx_INCREF(((PyObject *)__pyx_v_time_vector));\n",
       "  __Pyx_GIVEREF(((PyObject *)__pyx_v_time_vector));\n",
       "  PyTuple_SET_ITEM(__pyx_t_1, 0, ((PyObject *)__pyx_v_time_vector));\n",
       "  __pyx_t_3 = PyDict_New(); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __pyx_t_4 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __pyx_t_5 = __Pyx_PyObject_GetAttrStr(__pyx_t_4, __pyx_n_s_float); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "  if (PyDict_SetItem(__pyx_t_3, __pyx_n_s_dtype, __pyx_t_5) < 0) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;\n",
       "  __pyx_t_5 = __Pyx_PyObject_Call(__pyx_t_2, __pyx_t_1, __pyx_t_3); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;\n",
       "  if (!(likely(((__pyx_t_5) == Py_None) || likely(__Pyx_TypeTest(__pyx_t_5, __pyx_ptype_5numpy_ndarray))))) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __pyx_t_6 = ((PyArrayObject *)__pyx_t_5);\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_out.rcbuffer->pybuffer, (PyObject*)__pyx_t_6, &__Pyx_TypeInfo_nn___pyx_t_5numpy_float64_t, PyBUF_FORMAT| PyBUF_STRIDES| PyBUF_WRITABLE, 1, 0, __pyx_stack) == -1)) {\n",
       "      __pyx_v_out = ((PyArrayObject *)Py_None); __Pyx_INCREF(Py_None); __pyx_pybuffernd_out.rcbuffer->pybuffer.buf = NULL;\n",
       "      __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "    } else {__pyx_pybuffernd_out.diminfo[0].strides = __pyx_pybuffernd_out.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_out.diminfo[0].shape = __pyx_pybuffernd_out.rcbuffer->pybuffer.shape[0];\n",
       "    }\n",
       "  }\n",
       "  __pyx_t_6 = 0;\n",
       "  __pyx_v_out = ((PyArrayObject *)__pyx_t_5);\n",
       "  __pyx_t_5 = 0;\n",
       "
 18:     cdef double complex temp_sum
\n", "
+19:     for i in range(time_vector.shape[0]):
\n", "
  __pyx_t_7 = (__pyx_v_time_vector->dimensions[0]);\n",
       "  for (__pyx_t_8 = 0; __pyx_t_8 < __pyx_t_7; __pyx_t_8+=1) {\n",
       "    __pyx_v_i = __pyx_t_8;\n",
       "
+20:         temp_sum = 0
\n", "
    __pyx_v_temp_sum = __pyx_t_double_complex_from_parts(0, 0);\n",
       "
+21:         for j in range(weights.shape[0]):
\n", "
    __pyx_t_9 = (__pyx_v_weights->dimensions[0]);\n",
       "    for (__pyx_t_10 = 0; __pyx_t_10 < __pyx_t_9; __pyx_t_10+=1) {\n",
       "      __pyx_v_j = __pyx_t_10;\n",
       "
+22:             temp_sum += weights[j] * exp(I * time_vector[i] * omega )
\n", "
      __pyx_t_11 = __pyx_v_j;\n",
       "      __pyx_t_12 = __pyx_v_i;\n",
       "      __pyx_t_13 = __Pyx_c_prod_npy_float64(__Pyx_c_prod_npy_float64(__pyx_t_npy_float64_complex_from_parts(__Pyx_CREAL(__pyx_v_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_I), __Pyx_CIMAG(__pyx_v_46_cython_magic_180265c64445be1e3c6d6a7fae5a6faf_I)), __pyx_t_npy_float64_complex_from_parts((*__Pyx_BufPtrStrided1d(__pyx_t_5numpy_float64_t *, __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.buf, __pyx_t_12, __pyx_pybuffernd_time_vector.diminfo[0].strides)), 0)), __pyx_t_npy_float64_complex_from_parts(__pyx_v_omega, 0));\n",
       "      __pyx_v_temp_sum = __Pyx_c_sum_double(__pyx_v_temp_sum, __Pyx_c_prod_double((*__Pyx_BufPtrStrided1d(__pyx_t_double_complex *, __pyx_pybuffernd_weights.rcbuffer->pybuffer.buf, __pyx_t_11, __pyx_pybuffernd_weights.diminfo[0].strides)), std::exp(__pyx_t_double_complex_from_parts(__Pyx_CREAL(__pyx_t_13), __Pyx_CIMAG(__pyx_t_13)))));\n",
       "    }\n",
       "
+23:         out[i] = real(temp_sum)
\n", "
    __pyx_t_14 = __pyx_v_i;\n",
       "    *__Pyx_BufPtrStrided1d(__pyx_t_5numpy_float64_t *, __pyx_pybuffernd_out.rcbuffer->pybuffer.buf, __pyx_t_14, __pyx_pybuffernd_out.diminfo[0].strides) = std::real(__pyx_v_temp_sum);\n",
       "  }\n",
       "
+24:     return out
\n", "
  __Pyx_XDECREF(__pyx_r);\n",
       "  __Pyx_INCREF(((PyObject *)__pyx_v_out));\n",
       "  __pyx_r = ((PyObject *)__pyx_v_out);\n",
       "  goto __pyx_L0;\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a --cplus\n", "cimport numpy as np\n", "import numpy as np\n", "import cython\n", "\n", "cdef extern from \"\" namespace \"std\" nogil:\n", " double complex exp(double complex z)\n", " double real(double complex z)\n", "\n", "cdef double complex I = 1j \n", " \n", "@cython.boundscheck(False)\n", "@cython.wraparound(False)\n", "def synthesis_cython1(np.ndarray [np.complex128_t, ndim=1] weights,\n", " double omega,\n", " np.ndarray [np.float64_t, ndim=1] time_vector):\n", " cdef int i, j\n", " cdef np.ndarray [np.float64_t, ndim=1] out = np.empty_like(time_vector, dtype=np.float)\n", " cdef double complex temp_sum \n", " for i in range(time_vector.shape[0]):\n", " temp_sum = 0\n", " for j in range(weights.shape[0]):\n", " temp_sum += weights[j] * exp(I * time_vector[i] * omega )\n", " out[i] = real(temp_sum)\n", " return out" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": true }, "outputs": [], "source": [ "out = synthesis_cython1(weights, omega, time_vector)" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.allclose(ref_result, out)" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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\nlJ4+jRSCOSaWtoG6foE+bEWL9M/Lq+pZLFc4elN/rIn18f26EvzSU5nm5F8GPhf9+XPAS4MHCCEE\n8D/AMSnldzPc37DSq8a0Ito0qes9ffLyV0JN7oH1u7R8/DXXZn0qtsFS1S/Qh23i7Vq/FtvBwxlv\nq1+qetbP5W8PUHrmNH0WS9YnoYkn1f3SW5kG+W8Bq4QQ7wO3R18jhBgrhFgXHbMM+AxwmxBif/Tf\nPRnud4j0qjGdGX3S03VMn3r5K6Em98Lb2rcW68J5ZuEMSFW/QB+26ctmc6mgkJK2Vs6fPK8Dlbqe\n9XMd3LALa18fvvETcFeWmkylvl96K6MgL6X0SinrpJRTommd9uj7DVLKe6I/vyulFFLKOVLKedF/\n60becupyuVy6bGfc1GsIVFZTEOzhuA59bPTi0luxXJHd2reWq28zpytgrFT1C/RhszvsdE3X7jsd\nWa9PHxtVPevnatisPWRnW5j5PS49pLpfeitnnni1Wq26bat3jn55eT259FQ/l7fRi/v8WSJWG3MV\n+Cqtql+gH5vrRu0bU7tOeXlVPevn6tur1cePu2WRmTgDUt0vvZUzQd7v9+u2rcqb+uvl92W8LT25\n9FQ/14F12xFS4p88GVdJkclU6voF+rFNXqX9MbUfPERfX1/G21PVM7/fT0dLB6XnzhKxWplzp/nf\nFEFtv4xQzgT56upq3bY1I3pFq0deXk8uPdXP1RL9Kl2w2JzWwoOlql+gH9u0m2YRLHJR3NHOhwc/\nyHh7qnpWXV3NwTd2YpES/8RJSlxEgNp+GaGcCfLt7e26bWsgL38pyLHtmVVB6Mmlp/q5rPu0r9IT\nV5mfqgF1/QL92KxWK93RFhrH1m3LeHuqetbe3k7TO1rK07HI/Jv6/VLZLyOUM0E+0UNdqao/L386\nw7y83lx6SUrJ+ZPnKGlt4VJBITNvUeNDqKpfoC9b+c1aftr/7t6Mt6WqZ1JKRDQff90qc1sZxEpl\nv4xQzgR5vb/qVC7TOjFmWi+v8lfDw+u2A9A1YwZ2h91kIk2q+gX6ss362HIAXEeOEA5l9hCMqp6F\nLoYobWkiVFDIrNvUSAeCun7l0zUJ1NzcrOv2ZtyhT15eby691NzcTPtWrdtmyTLzWgsPlqp+gb5s\n10y7lkB1DQWXghx6J7MqG1U9q3/xbQA6Z87EUaDGRQSo65dRXDkT5JNp1JOKxk0dR6BKy8sfffdg\n2tvRm0svFRUVURh96vJ6k6b6iydV/QL92SI3aHXjH27IrL+8qp5d2qM9Z1K6XJ2LCFDXL6O4cibI\nG6HIfO1DeOr1zG+OqaYzB05TFPDTXepm8g3Xm40zKlV7q1ZSGNql/7yvZisSieA6chSA6fcsM5lm\ndCtngnxnZ6fu26yt065wL21P/+aYEVx66IPohOXBObOxWNT5NVDVL9Cfbf69y+izWHCf/gCfN/0a\naRU9O7btMM6uTjrLK7huziSzcS6Tin6BcVzqfLozVE3NiK3s09L8j/V/CE/ha0uvob8RXHoosk+7\nyqo0caq/eFLVL9CfzV3lxjdhIpa+Pg68nn6LAxU9e/8N7dtveME8pS4iQE2/wDgutdzPQK2trbpv\n013lxjdxMpa+Pva9ml7KxgiuTNXT2UPp0WMAzHngFpNpLpeKfvXLCDbH4vkANLwddx6dpKSiZz07\ntBTUVSvVeMo1Vir6BcZx5UyQ1zoa66+Cm7Qr3aaN6d0cM4orEx3YsAt7OIRv3DWMmaBW21UV/eqX\nEWwT7tDqxy170y/VVc2zrkA37pMnkUIw72Pq5eNV86tfRnHlTJCvqDBmXtLJd2u/pLa9+9LqM2IU\nVyY6u0FLDVgUaWUQKxX96pcRbLNvu4FQQSGlzU1cOJVe62HVPNu/bmd0qr/xVI6pNBtniFTzq19G\nceVMkDfqq86M5XO0PiPt3rT6jKj41VDu0m4kj79TvassFf3qlyHpmgI7nTOmA3Do5XfT2oZqnl2I\nfuuNzJ1hMkl8qeZXv/LpmgQqLTVmMgKb3Ub33DkAHH019Q+hUVzp6vzJ87gbGwg5Cpi7So3Wr7FS\nza9YGcVWGm1x0L45vby8ap7JPVr31nEKzE8QT6r51S+juHImyEciEcO2XRm9eRSIPiGaiozkSkeH\nXt4CgH/GDAqcBSbTDJVqfsXKKLZZ92s3v10HDhK6lHqLA5U8O3/yHO6GC4QKCpm6YpbZOHGlkl+x\nMooroyAvhKgQQrwphHg/+v+wE4QKIaxCiH1CiFcz2edw6urqMmKzAMy5bwUAJUePEuwOprSukVzp\nqCN6tehYNNtkkvhSza9YGcU2YdZ1+GtqKQj2cGBD6g3xVPLswIubAOicM5twr5oTZqvkV6yM4sr0\nSv5JYKOUcgqwMfp6OP0xcCzD/Q0rIyfnHTNxDL6x47CHQxzYkNrVvEqTBocuhXEdPATA/IfrTKaJ\nL5X8GixD2ZZqKZszr6eeElTJM9872k398luXKMUVq9HGlWmQfwD4WfTnnwEPxhskhBgH3Av8KMP9\nDSujJ+cVN2r1zJ43UvsQqjRp8KG391JwKYj/qloc5Q6zceJKJb8Gy0i28XdpXSn7du5JeV1VPOvp\n7KHkiNavZt6DK5XhGqzRxpVpkK+RUjZGf24Chntk63vAnwOZz3U2jOx2Y7vcjb9b+xCyI7UreaO5\nUtHp6FOI8sYFSnHFSlUuMJZt3p03EnIU4G64wPmT51JaVxXP9r2+A3s4zMVrrmXMxDHKcA3WaOOy\nJRoghHgLiPc94i9jX0gppRBiSNd7IcTHgBYp5V4hxMqR9tXS0sKaNWuw2WxEIhFWr17N2rVraWpq\nwuVyYbVa8fv9VFdX097ejpSS6upqmpubsdvteL1eOjs7qampobW1FSEEFRUVtLa2UlpaSiQSoaur\ni9raWpqamrDb7bjdbtra2nC73YRCIXp6egaWOxwOSkpK8Hq9TLppBtsKnZQ2N3Fi73EKq5wUFhbi\ndDrp6OigsrKSQCBAKBQaWN/pdGKxWPB4PFRVVeHz+QiHwwPLEx1Tf1c6vY6p910t33vVyhvo7u7G\n7/fT09NDMBgcWD+ZY3I4HPh8PkOOyeFw0NbWlvZ5Ki8vN+yYuru76e7uNuw8+WbOoHrfPrb/4jVu\n/uIDSR9TUVERjY2NWT1P8Y7p9MvvUAX0LpjNxYsXiUQieDyerJ+nRMfU3d1NMBjUPUZkekwul4uG\nhoa0jmnEGJ7JbCRCiBPASilloxBiDLBJSnn9oDHfBD4D9AKFQCnwgpTydwZvb8eOHXLatGlpsXg8\nHsaPH5/Wusnq1w/9KRU7dhD6wu9x/9c/rwxXMjp/8jyHV3yCsKOA24+uo8XbogTXYKniVzwZzfb6\nd3+D+M6/0T5/Po+9/gNluJJRX18fz89+iBJvKzU//z7z71ikBFc85SJXfX393rq6uoXxlmWarnkZ\n+Fz0588BLw0eIKX8mpRynJRyAvAp4O14AT5Tud1uvTc5RNWrtIeHOjcl3+IgG1zJaP/zGwEIzJ2D\ns9ipDNdgqcoFxrPNe3AlACWHj9DT2ZP0eip4dnr/KUq8rQSLXMy+Vbt/pQJXPI02rkyD/LeAVUKI\n94Hbo68RQowVQqzLFC4VhUIhw/ex8JFbta6UJ44n3ZUyG1zJyL9Rm+qv8nbtD5UqXIOlKhcYzzZm\n4hgujrsWezjEvjeSv5BQwbMj0ecvuufPxWbXssAqcMXTaOPKKMhLKb1Syjop5RQp5e1Syvbo+w1S\nymmQtL4AABj1SURBVHvijN8kpfxYJvscTj09yV/5pKvyq8rxTZ6Kta+PPc9vSmqdbHAlks/rx33s\nKH1CsODhWwE1uOJJVS7IDpttqfaN+9xrW5JeRwXPujZppZPVdTcNvKcCVzyNNq6ceeI1W7WvRSu1\niUSaNyRXSqlCTe7e327GGongmzSZ6nHaZMEqcMWTqlyQHbapq28DwLbjvaQb4pntmbfRS9nJE0Qs\nFhY+cuvA+2ZzDafRxpUzQT5bta+zVmu/xEX1+wiHEj/Rp0JNbtN67Q+S85alH72nAFc8qcoF2WGb\nuWIuXe4yXBc7OLw5ufbDZnu255mNWPr68E2fQflVHz30bjbXcBptXDkT5B2O7DzcM2neFPxX1VDY\n083+JJ5+zRbXcAqHwhTt0SZwmPnwbQPvm801nFTlguywWa1WwjctBuDECxuTWsdsz7zrtwJQevvl\nXU3N5hpOo40rZ4J8SUlJ9nZ2k9aw7PRLbyccmlWuODrw1h4Ku7sIVNcwad7kgffN5hpOqnJB9tgm\n3K99W5RbkpsS0EzPOn2dlB48CMCCT9x+2TJVz+Vo48qZIO/1erO2rymrtV9m29YdCfOm2eSKpw9+\nq/0hkksXXTbXptlcw0lVLsge24J7lhJ0FlHa3MT79ScTjjfTsz0vbsHWG6bjuomMnXT1ZctUPZej\njStngnx5+bANMHXXnLoFdJWV47rYwaF39o04NptcgxWJRLBt0Uonpzyy6rJlZnKNJFW5IHtsjgI7\n3Yu0aScPP/NmwvFmetb42iYACm+9acgyVc/laOPKmSCfzbIoq9VK73LtJuaJZzeMONbMcq1Db9fj\nuthBV1k5c+oun+pvtJWR6aFsso29V+sxf2nT9oRjzfIsdOmj+z2zH7l9yHJVz+Vo48qZIB8Mptbn\nPVNNflj7pbZs2T5iyibbXLE6+ax2Fdh781KsVutly8zkGkmqckF22RatXknYbqfszIcJ5341y7O9\nr2yjoKcbf+0YJi+YOmS5qudytHHlTJDPdu3rvDsW0V3qprjdy5GtB4cdZ1ZNbl9fH5at8VM1MPpq\nhfVQNtlcJUUE5s0DYM8v3xhxrFmenXle+xZrvXV53OWqnsvRxpUzQT7bta9Wq5XQMu3BqOPPDp83\nNasm9+Db9RR3tNPlLmPu7UP7Fo22WmE9lG222ge0iV163nhnxHFmeBbsDlK0U+tqOvuxu+KOUfVc\njjaunAnyhYWFWd/npGiVDZveHTZlYwYXwMnn+lM1Nw1J1YB5XImkKhdkn23JJ1cRchRQduZDTh84\nNew4Mzx774XNFPR047t6HFMXTY87RtVzOdq4cibIO53OrO9z/l2L6S4ppaStddinE83gikQiWDZr\nE4RMeXhoqgbM4UpGqnJB9tmKSpx03qhNC7jvl8P3+zPDs/MvahcRjjtXDjtG1XM52rhyJsh3dHRk\nfZ82u43wSi0fefQX8ecnN4Nr72s7KO5op7O8grl3xG0xbQpXMlKVC8xhuzZ6PyXy5uZhvy1mm6sr\n0E3J7r0AzP+dIX0IB6TquRxtXDkT5CsrK03Z78zP3AdAweZthC4N7WVjBtcHv9Gu+uQdt8ZN1YB5\nfiWSqlxgDtuND60kWOSitKmR4zuOxB2Tba6dT7+FPXSJjusmMmHGhGHHqXouRxtXzgT5QCBgyn5n\nLJ+Nv3Yszq4Au54beoMs21xdgW5c27Ve5PM/d9+w48zyK5FU5QJz2BwFdoLLtQeNjvwqfsom21zN\nz2vVPq67Vo44TtVzOdq4cibImzURgMViwX6X1mvk3DOvD1meba4dv96A41KQjgkT49Yu92u0TZyg\nh8xim/wprXrF+tamuJ1Ps8nVeKaJsgMHiFgsLF3zwIhjVT2Xo40rZ4K8mbWvi37vfgBK9+4dMmNU\ntrlaoldZJfcPfQIxVqOtVlgPmcW24K7FBKprKPL72Pns0G+L2eTa9cPfYpES34IFA3MTDCdVz+Vo\n48qZIG9m7eu4qdfQcf00bL29bPvJK5ctyyZXk6eJssOHiVgsLHl85Am4RlutsB4yi81isWC77w4A\nzv36lSHLs8XV19dH6BXtAahrHk08wZuq53K0cWUU5IUQFUKIN4UQ70f/j9thRwhRJoR4TghxXAhx\nTAixNN64TGR2WVTVJ+8FwP/Mq5dVQWSTa/sPntMmb1iwgKqxVSOONduv4aQqF5jLtvQPH9LmF963\njybP5cEgW1wH3tpLaUsT3SWlLPn4rQnHq3ouRxtXplfyTwIbpZRTgI3R1/H0feANKeU0YC5wLMP9\nDpHZEwEs/+w9BItclJ07e1nNfLa4esO99L2ipWomPP5QwvFm+zWcVOUCc9lqx9dyce48rH197Pzv\n3162LFtcx3+q7Tey6lbsDnvC8aqey9HGlWmQfwD4WfTnnwEPDh4ghHADK4D/AZBShqSUFzPc7xD5\nfL7EgwyUs9hJ6A5t5qUj//3cwPvZ4tr57DsUd7QTqKpm8UMrEo4326/hpCoXmM827jGtWir0ynoi\nkcjA+9ng8ja1U7xVm0Zy/ucTX0SA+X4Np9HGlWmQr5FSNkZ/bgJq4oy5DmgFfiKE2CeE+JEQwpXh\nfoeoqmrk9EQ2tPALHweg+N3tXGzRHmzIFtfZn78IgP3Bu4etjY+VCn7Fk6pcYD7b0k/eRpe7jNKW\nZna9sHng/Wxwbf3Bs9jDYdpnzhqxaitWZvs1nEYbly3RACHEW0C8275/GftCSimFEHKYfSwAviyl\n3CWE+D5aWuevBw9saWlhzZo12Gw2IpEIq1evZu3atTQ1NeFyubBarfj9fqqrq2lvb0dKSXV1Nc3N\nzYRCISoqKujs7KSmpobW1laEEFRUVNDa2kppaSmRSISuri5qa2tpamrCbrfjdrtpa2vD7XYTCoXo\n6ekZWO5wOCgpKcHr9VJeXk5PTw/BYHBgeWFhIU6nk46ODiorK3Fe5aLt+mlUnTjOG9/9Jbf9yScJ\nBALYbDaqqqrw+XyEw+GB9RMdU3FxMUDCYzp96AOtrM1qY8rqFXR1dSU8pjNnznDttdcmPKZAIEAo\nFBpY7nQ6cTgc+Hw+Q44pHA5TVlZm6HlK95guXLjAxIkT0z5PehwTD94DP/s1p596mll3LqSjo4O+\nvj4KCwsNO0+VFZWEn3+VQqD2Mw/g8XiSOqaWlhYKCgqyfp4SHVNjYyOTJ082JUaMdExSyrSPacQY\nLmW8uJychBAngJVSykYhxBhgk5Ty+kFjaoGdUsoJ0dc3A09KKe8dvL0dO3bIadOmpcXi8XgYP358\nWuvqqU0/W0fwq/9IZ0UlD+x/gYbGBsO5/nfNP+B+7XU6bl7Oo89+J6l1VPFrsFTlAjXYvI1ett+4\nGns4zKR1P2XKgqmGc2355Xq6v/J3BCqreWj/89jsCa8NATX8iqdc5Kqvr99bV1cXt4dJpumal4HP\nRX/+HPDS4AFSyibgnBCiP/jXAUcz3O8QqVL7evPv3In/qhqK271s/slrhnN5m9op2rARgHn/97NJ\nr6eKX4OlKheowVY5ppLOldo9l73f/xVgPNeFH/4vAI6H7006wIMafsXTaOPKNMh/C1glhHgfuD36\nGiHEWCFE7DPYXwZ+JYQ4CMwD/inD/Q6RKrWvVquV4s88DEDbj56moaHB0P1t+d6vsYdDtM+azfSb\nZiW9nip+DZaqXKAO2w1/9BgArrc30eJpNpSrfv0uyk8c51Khk5V/9MmU1lXFr8EabVwZBXkppVdK\nWSelnCKlvF1K2R59v0FKeU/MuP1SyoVSyjlSygellLq3W3O5dL+Xm7Zu/cLD9BSXUHbuLKff1b1a\ndEBdgW7k81r3y4lffDSldVXyK1aqcoE6bFMXTad9/gLs4TCbvvljQ7mOf/enAIQfuAd3lTuldVXx\na7BGG1fOPPGaTEVJtuQsdsIjWrlb83/8ZsQ5YDPRm9/+OUUBPxfHT+DGBxOXTcZKJb9ipSoXqMU2\n+8nPA+B8bT2+FmNK745sPUDFgQOE7Q5WfvVziVcYJJX8itVo48qZIO/3+81GuEx1f/45elwlVHz4\nAdt+85bu2/d5/fD/t3fm0VEUeQD+fkwSEiDXQDgWEPBAUVjwZlFQxPtARUFczxXlKTzPRUVZXZ/i\nCbKH59MVF09UVHTd9cx64kYUOQQFCUgQTMIkkxByX7V/dAcnyWTIXD3FTH3vzUtPTXf1179qfnRX\nd3W/9DoA+8y6ki5dgmtK3eLVgq5eoJfbiONG4R05iuSGev634MWorGP13CcBqD7tpD2OoPaHTvHy\nJdG84ibJ5+QEfliS02S40+lyiXXf/C8L/tFq8Eok+Oi+Z0mtrqLsgKGMPu/4oJfXLV4t6OoF+rkN\nt4/ms97L5ef1WyNa91dLP8e9ejX1XVMZf8dVIdWhW7xaSDSvuEnyXq831grtOHnWRVRlZpO5fRsf\nPrJkzwt0km0/biPlVWvw0/63Tg/6KB70jBfo6wX6uf12/GF4x4whqbGBz2f/NWL1NjU18dN9jwPQ\neMEkeg/sHVI9usWrhUTzipskH879/tEirUcaXa6wjuZrHnmG0qLINOJns+Zbd9QcdRRHnjkmpDp0\njBfo6wV6uo174Hoak5Jx5+Wx4t28iNT5/oKXydpaQHVGFqfccUXI9egYL0g8r7hJ8rqegp1y7RTK\nhh5EWlUlH9z8l7Dr+/K1/+JevpyG5BTGzZ8Vcj26xktXL9DTbcDQgVRPst5nsOm2edRU1oRVX+GW\nIhoeexaAzJuvpnt6t5Dr0jFekHhecZPki4uLY63gF4/Hw+EP30yTy0X2h7l8sTj0i7ClhaUUzZkP\nQOMlFzBg6ICQ69I1Xrp6gb5uI685i4refckoKuRfs0Lvtmlububja+bStbYG78iRjJ+252fGB0LX\neCWaV9wk+c48wyEW9OjRg6FHDqP+MmsAi+dP89hREHxjNjc38/70u+lWUU7Zfvtz5l1Xhu2lI7p6\ngb5uvXr3ZOiC22kWIf2td8h745OQ6nnnnoW4V35LXWoaY/92W0jXenzRNV6J5hU3SV53Jt4zHe9B\nw0ir3EXuhbOo3hXcafWbtz6K++uvqe+ayu+evrtTz/M2JA6jTjyCmqnnI0pR9Me5bF6zOajll7/9\nBa6nrKeGZ/75JgYetE80NA0xIG6SfGVlZawV/NLi5XK5OGHRvVRmu8nevImlU2+hrqauU3W8O/8l\nuj+/GCVC1r23MvjgwRHz0g1dvUBftxavc+Zfh/fQQ0mtqWbF729k24/bOrX8us9XU3jtnbiamtg5\n8QyO/0O7ZweG5aUbieYVN0m+Tx9/j7KPPb5efQf1ZcSiedSlpuFesYLXz7x293Pn/dHc3MzSOU8i\n8x8FoOGaKxh38SkR99IJXb1AX7cWL5fLxRkv3E/5wEGkl3jIO3cG6/PWBVw2741P2HTxjXStq8V7\n9NGc99itEffSjUTzipsk7/F4Yq3gl7ZeBx41jP2eW2CNhl23ltxxF/PxwnfaDZbasPwHFp98DanP\nPAdA/YxpTLxzWtS8dEFXL9DXzdcrs2cGp/z7ccoHD6FHaQkbJ8/gzdufoGpXdetltnl45aq5lM2c\nQ4qd4Ce9fH9QT5kMxksnEs0rci0aY0Qk1gp+8ec1fNxIur/1BHlX3kn2ls3U3X4fSx96groDh0LX\nFLps2UrW1gLcQF1aNzLvuolTLzu9feUR9tIBXb1AX7e2Xtm9sznrw6d5+8p7yP70U5IXPk/uC69S\nOWwYkpWJ2uEhY+OPZDY20ixC1dTzueDh6yP+7JS9JV66EC2vsF4aEmnCeWlIdXU13bqFfk9vtAjk\n1VDfwHvzXqTuhSX0KGs9UKohpStV48dxwt0z6D0o8qdxe2O8Yo2uboG8vlzyMQULFpK9eVO737yj\nRjHitumMOG6U416xJB69Ar00JG6O5D0ej5ZvewnklZySzFlzLqdp9iWs/3Itv3yXT2NtPdlD+jPi\nxCPCGogSjlcs0dUL9HUL5DXm/PGMOX88W9b+xE/L11HrrSAtJ4tDJhxJzoDoDgraG+MVS6LlFTdJ\nPiMjI9YKfumMl8vl4pCxIzlk7EgHjCz25njFCl3dOuM1ePgQBg8f4oDNr+zN8YoF0fKKmwuvkX7K\nY6QwXsGhqxfo62a8giPRvOImyVdVVcVawS/GKzh09QJ93YxXcCSaV1hJXkTcIvKhiGy0/2Z3MN+N\nIrJORNaKyMsikhrOev2RaC/nDRfjFTy6uhmv4Eg0r3CP5GcDuUqpA4Bc+3srRKQ/cB1whFJqOOAC\npoa53nYk2st5w8V4BY+ubsYrOBLNK9wkfzawyJ5eBJzTwXxJQJqIJAHdgF/CXG87li5dGukqI4Lx\nCg5dvUBfN+MVHInmFe7dNX2UUoX2dBHQ7oZupdR2EZkPbAVqgA+UUh/4q2zHjh1MmzaNpKQkmpqa\nmDRpEjNnzqSoqIju3bvjcrmoqKggJycHr9eLUoqcnByKi4v5/vvvKS0tpbKykj59+uDxeBAR3G43\nHo+HjIwMmpqaqKqqom/fvhQVFZGcnExmZiYlJSVkZmZSX19PTU3N7t9TUlJIT0+ntLSU7Oxsampq\nqK2t3f17amoqaWlplJWV0bNnT3bt2kV9ff3u39PS0li5ciUFBQX06tWLnTt30tDQsPv3PW1Ty1Pp\norFN5eXlVFRUhLRNKSkp7Ny5MyrbtGbNGkpKShxvp85sU3l5OdXV1Y62U2e2KS8vjylTpjjaTp3Z\npvz8fAoKChxvpz1tU3l5ObW1tdrkiJZtWrZsGZMnTw5pmwKxx8FQIvIR4K+zaA6wSCmV5TNvmVKq\nVb+83U//OnABUA68BixRSr3QtsJwBkMdc8wxLFu2LKRlo4nxCg5dvUBfN+MVHPHoFWgwVFgjXkVk\nA3C8UqpQRPoBnyilDmwzz2TgVKXUNPv7pcBopdSMtvXl5uZ6gIJQXLxeby+3210SyrLRxHgFh65e\noK+b8QqOOPUaNGHCBL+j28LtrnkbuAx4wP77lp95tgKjRaQbVnfNBOAbf5V1JGkwGAyG0Aj3wusD\nwEkishE40f6OiPxGRP4DoJT6ClgCfAt8Z6/zqTDXazAYDIZOoNUDygwGg8EQWeJixKuInCoiG0Qk\nX0Ta3asf5XUPFJGPReR7e8DX9Xb5XSKyXURW2Z/TfZa5zXbdICKReQuIf7ctIvKdvf5v7LIOB7A5\n4SUiB/rEZJWIVIjIDbGIl4gsFJEdIrLWpyzo+IjI4Xac80Xk7xLmM2M78JonIutFZI2IvCkiWXb5\nYBGp8Ynbkw57Bd1uDnm94uO0RURW2eVOxquj3ODsPqaU2qs/WIOrNgH7AinAauBgB9ffDzjMnk4H\nfgQOBu4CZvmZ/2DbsSswxHZ3RcltC9CrTdlDwGx7ejbwoNNebdquCBgUi3gB44DDgLXhxAdYDowG\nBHgXOC0KXicDSfb0gz5eg33na1OPE15Bt5sTXm1+fxi4Mwbx6ig3OLqPxcOR/FFAvlJqs1KqHliM\nNUjLEZRShUqpb+3pXcAPQP8Ai5wNLFZK1SmlfgLysbbBKToawBYLrwnAJqVUoDuqouallPoM8LYp\nDio+Yt1VlqGUylPWv8bn6HhQYMheSqkPlFKN9tc8YECgOpzyCkBM49WCfcQ7BXg5UB1R8uooNzi6\nj8VDku8P/OzzfRuBk2zUEJHBwKHAV3bRtfbp9UKfUzInfRXwkYisEJHpdllHA9hiEceptP7HF+t4\nQfDx6W9PO+UHcAXW0VwLQ+yuh09FZKxd5qRXMO3mdLzGAsVKqY0+ZY7Hq01ucHQfi4ckrwUi0gNr\n0NcNSqkK4AmsLqRRQCHWKaPTHKuUGgWcBswUkXG+P9pHBTG58i4iKcBErMFxoEe8WhHL+HSEiMwB\nGoEX7aJCYB+7nW8CXhIRJx+Yrl27teFCWh9IOB4vP7lhN07sY/GQ5LcDA32+D7DLHENEkrEa8UWl\n1BsASqlipVSTUqoZeJpfuxgc81VKbbf/7gDetB2K7dO/llPUHU572ZwGfKuUKrYdYx4vm2Djs53W\nXSdR8xORy4EzgYvs5IB9al9qT6/A6scd6pRXCO3mZLySgEnAKz6+jsbLX27A4X0sHpL818ABIjLE\nPjqcijVIyxHsPr9ngB+UUgt8yvv5zHYu0HLl/21gqoh0FZEhwAFYF1Ui7dVdRNJbprEu3K3l1wFs\n0HoAmyNePrQ6wop1vHwIKj72aXeFiIy294VL8T8oMCxE5FTgFmCiUqrapzxHRFz29L6212YHvYJq\nN6e8bE4E1iuldnd1OBmvjnIDTu9j4Vw91uUDnI515XoTMMfhdR+Ldbq1Blhlf04Hnsca/LXGbrx+\nPsvMsV03EOYV/ABe+2JdqV8NrGuJC9AT67HQG4GPALeTXvZ6ugOlQKZPmePxwvpPphBowOrnnBZK\nfIAjsJLbJuBR7PEnEfbKx+qvbdnHnrTnPc9u31VYAw7Pctgr6HZzwssu/ydwdZt5nYxXR7nB0X3M\nDIYyGAyGOCYeumsMBoPB0AEmyRsMBkMcY5K8wWAwxDEmyRsMBkMcY5K8wWAwxDEmyRsMBkMcY5K8\nwWAwxDEmyRsMBkMc83/TkA20gTifAQAAAABJRU5ErkJggg==\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plt.plot(ref_result)\n", "plt.plot(out)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's now do some timings." ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "30.1 ms ± 625 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%timeit synthesis(weights, omega, time_vector)" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "127 ms ± 5.81 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%timeit synthesis_cython1(weights, omega, time_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, our Cython version is almost ten times slower than our vectorized implementation. Let's try tweaking it." ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_59850e01233c87321a5c3910c7be7295.pyx\n", " \n", " \n", "\n", "\n", "

Generated by Cython 0.25.2

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
+01: cimport numpy as np
\n", "
  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
+02: import numpy as np
\n", "
  __pyx_t_1 = __Pyx_Import(__pyx_n_s_numpy, 0, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_np, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 03: import cython
\n", "
 04: 
\n", "
 05: cdef extern from "<complex.h>" namespace "std" nogil:
\n", "
 06:     double complex exp(double complex z)
\n", "
 07:     double real(double complex z)
\n", "
 08: 
\n", "
+09: cdef double complex I = 1j
\n", "
  __pyx_v_46_cython_magic_59850e01233c87321a5c3910c7be7295_I = __pyx_t_double_complex_from_parts(0, 1.0);\n",
       "
 10: 
\n", "
 11: @cython.boundscheck(False)
\n", "
 12: @cython.wraparound(False)
\n", "
+13: def synthesis_cython2(np.ndarray [np.complex128_t, ndim=1] weights,
\n", "
/* Python wrapper */\n",
       "static PyObject *__pyx_pw_46_cython_magic_59850e01233c87321a5c3910c7be7295_1synthesis_cython2(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/\n",
       "static PyMethodDef __pyx_mdef_46_cython_magic_59850e01233c87321a5c3910c7be7295_1synthesis_cython2 = {\"synthesis_cython2\", (PyCFunction)__pyx_pw_46_cython_magic_59850e01233c87321a5c3910c7be7295_1synthesis_cython2, METH_VARARGS|METH_KEYWORDS, 0};\n",
       "static PyObject *__pyx_pw_46_cython_magic_59850e01233c87321a5c3910c7be7295_1synthesis_cython2(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {\n",
       "  PyArrayObject *__pyx_v_weights = 0;\n",
       "  double __pyx_v_omega;\n",
       "  PyArrayObject *__pyx_v_time_vector = 0;\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"synthesis_cython2 (wrapper)\", 0);\n",
       "  {\n",
       "    static PyObject **__pyx_pyargnames[] = {&__pyx_n_s_weights,&__pyx_n_s_omega,&__pyx_n_s_time_vector,0};\n",
       "    PyObject* values[3] = {0,0,0};\n",
       "    if (unlikely(__pyx_kwds)) {\n",
       "      Py_ssize_t kw_args;\n",
       "      const Py_ssize_t pos_args = PyTuple_GET_SIZE(__pyx_args);\n",
       "      switch (pos_args) {\n",
       "        case  3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);\n",
       "        case  2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "        case  1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "        case  0: break;\n",
       "        default: goto __pyx_L5_argtuple_error;\n",
       "      }\n",
       "      kw_args = PyDict_Size(__pyx_kwds);\n",
       "      switch (pos_args) {\n",
       "        case  0:\n",
       "        if (likely((values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_weights)) != 0)) kw_args--;\n",
       "        else goto __pyx_L5_argtuple_error;\n",
       "        case  1:\n",
       "        if (likely((values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_omega)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"synthesis_cython2\", 1, 3, 3, 1); __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "        }\n",
       "        case  2:\n",
       "        if (likely((values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_time_vector)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"synthesis_cython2\", 1, 3, 3, 2); __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "        }\n",
       "      }\n",
       "      if (unlikely(kw_args > 0)) {\n",
       "        if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, pos_args, \"synthesis_cython2\") < 0)) __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "      }\n",
       "    } else if (PyTuple_GET_SIZE(__pyx_args) != 3) {\n",
       "      goto __pyx_L5_argtuple_error;\n",
       "    } else {\n",
       "      values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "      values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "      values[2] = PyTuple_GET_ITEM(__pyx_args, 2);\n",
       "    }\n",
       "    __pyx_v_weights = ((PyArrayObject *)values[0]);\n",
       "    __pyx_v_omega = __pyx_PyFloat_AsDouble(values[1]); if (unlikely((__pyx_v_omega == (double)-1) && PyErr_Occurred())) __PYX_ERR(0, 14, __pyx_L3_error)\n",
       "    __pyx_v_time_vector = ((PyArrayObject *)values[2]);\n",
       "  }\n",
       "  goto __pyx_L4_argument_unpacking_done;\n",
       "  __pyx_L5_argtuple_error:;\n",
       "  __Pyx_RaiseArgtupleInvalid(\"synthesis_cython2\", 1, 3, 3, PyTuple_GET_SIZE(__pyx_args)); __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "  __pyx_L3_error:;\n",
       "  __Pyx_AddTraceback(\"_cython_magic_59850e01233c87321a5c3910c7be7295.synthesis_cython2\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return NULL;\n",
       "  __pyx_L4_argument_unpacking_done:;\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_weights), __pyx_ptype_5numpy_ndarray, 1, \"weights\", 0))) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_time_vector), __pyx_ptype_5numpy_ndarray, 1, \"time_vector\", 0))) __PYX_ERR(0, 15, __pyx_L1_error)\n",
       "  __pyx_r = __pyx_pf_46_cython_magic_59850e01233c87321a5c3910c7be7295_synthesis_cython2(__pyx_self, __pyx_v_weights, __pyx_v_omega, __pyx_v_time_vector);\n",
       "\n",
       "  /* function exit code */\n",
       "  goto __pyx_L0;\n",
       "  __pyx_L1_error:;\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_46_cython_magic_59850e01233c87321a5c3910c7be7295_synthesis_cython2(CYTHON_UNUSED PyObject *__pyx_self, PyArrayObject *__pyx_v_weights, double __pyx_v_omega, PyArrayObject *__pyx_v_time_vector) {\n",
       "  int __pyx_v_i;\n",
       "  int __pyx_v_j;\n",
       "  PyArrayObject *__pyx_v_out = 0;\n",
       "  __pyx_t_double_complex __pyx_v_temp_sum;\n",
       "  __pyx_t_double_complex __pyx_v_temp_mult;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_out;\n",
       "  __Pyx_Buffer __pyx_pybuffer_out;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_time_vector;\n",
       "  __Pyx_Buffer __pyx_pybuffer_time_vector;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_weights;\n",
       "  __Pyx_Buffer __pyx_pybuffer_weights;\n",
       "  PyObject *__pyx_r = NULL;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"synthesis_cython2\", 0);\n",
       "  __pyx_pybuffer_out.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_out.refcount = 0;\n",
       "  __pyx_pybuffernd_out.data = NULL;\n",
       "  __pyx_pybuffernd_out.rcbuffer = &__pyx_pybuffer_out;\n",
       "  __pyx_pybuffer_weights.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_weights.refcount = 0;\n",
       "  __pyx_pybuffernd_weights.data = NULL;\n",
       "  __pyx_pybuffernd_weights.rcbuffer = &__pyx_pybuffer_weights;\n",
       "  __pyx_pybuffer_time_vector.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_time_vector.refcount = 0;\n",
       "  __pyx_pybuffernd_time_vector.data = NULL;\n",
       "  __pyx_pybuffernd_time_vector.rcbuffer = &__pyx_pybuffer_time_vector;\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_weights.rcbuffer->pybuffer, (PyObject*)__pyx_v_weights, &__Pyx_TypeInfo___pyx_t_double_complex, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_weights.diminfo[0].strides = __pyx_pybuffernd_weights.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_weights.diminfo[0].shape = __pyx_pybuffernd_weights.rcbuffer->pybuffer.shape[0];\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer, (PyObject*)__pyx_v_time_vector, &__Pyx_TypeInfo_nn___pyx_t_5numpy_float64_t, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_time_vector.diminfo[0].strides = __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_time_vector.diminfo[0].shape = __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.shape[0];\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_L1_error:;\n",
       "  __Pyx_XDECREF(__pyx_t_1);\n",
       "  __Pyx_XDECREF(__pyx_t_2);\n",
       "  __Pyx_XDECREF(__pyx_t_3);\n",
       "  __Pyx_XDECREF(__pyx_t_4);\n",
       "  __Pyx_XDECREF(__pyx_t_5);\n",
       "  { PyObject *__pyx_type, *__pyx_value, *__pyx_tb;\n",
       "    __Pyx_PyThreadState_declare\n",
       "    __Pyx_PyThreadState_assign\n",
       "    __Pyx_ErrFetch(&__pyx_type, &__pyx_value, &__pyx_tb);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __Pyx_ErrRestore(__pyx_type, __pyx_value, __pyx_tb);}\n",
       "  __Pyx_AddTraceback(\"_cython_magic_59850e01233c87321a5c3910c7be7295.synthesis_cython2\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  goto __pyx_L2;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __pyx_L2:;\n",
       "  __Pyx_XDECREF((PyObject *)__pyx_v_out);\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "/* … */\n",
       "  __pyx_tuple__10 = PyTuple_Pack(8, __pyx_n_s_weights, __pyx_n_s_omega, __pyx_n_s_time_vector, __pyx_n_s_i, __pyx_n_s_j, __pyx_n_s_out, __pyx_n_s_temp_sum, __pyx_n_s_temp_mult); if (unlikely(!__pyx_tuple__10)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_tuple__10);\n",
       "  __Pyx_GIVEREF(__pyx_tuple__10);\n",
       "/* … */\n",
       "  __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_46_cython_magic_59850e01233c87321a5c3910c7be7295_1synthesis_cython2, NULL, __pyx_n_s_cython_magic_59850e01233c87321a); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_synthesis_cython2, __pyx_t_1) < 0) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 14:                      double omega,
\n", "
 15:                      np.ndarray [np.float64_t, ndim=1] time_vector):
\n", "
 16:     cdef int i, j
\n", "
+17:     cdef np.ndarray [np.float64_t, ndim=1] out = np.empty_like(time_vector, dtype=np.float)
\n", "
  __pyx_t_1 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __pyx_t_2 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_empty_like); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_2);\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __pyx_t_1 = PyTuple_New(1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __Pyx_INCREF(((PyObject *)__pyx_v_time_vector));\n",
       "  __Pyx_GIVEREF(((PyObject *)__pyx_v_time_vector));\n",
       "  PyTuple_SET_ITEM(__pyx_t_1, 0, ((PyObject *)__pyx_v_time_vector));\n",
       "  __pyx_t_3 = PyDict_New(); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __pyx_t_4 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __pyx_t_5 = __Pyx_PyObject_GetAttrStr(__pyx_t_4, __pyx_n_s_float); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "  if (PyDict_SetItem(__pyx_t_3, __pyx_n_s_dtype, __pyx_t_5) < 0) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;\n",
       "  __pyx_t_5 = __Pyx_PyObject_Call(__pyx_t_2, __pyx_t_1, __pyx_t_3); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;\n",
       "  if (!(likely(((__pyx_t_5) == Py_None) || likely(__Pyx_TypeTest(__pyx_t_5, __pyx_ptype_5numpy_ndarray))))) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __pyx_t_6 = ((PyArrayObject *)__pyx_t_5);\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_out.rcbuffer->pybuffer, (PyObject*)__pyx_t_6, &__Pyx_TypeInfo_nn___pyx_t_5numpy_float64_t, PyBUF_FORMAT| PyBUF_STRIDES| PyBUF_WRITABLE, 1, 0, __pyx_stack) == -1)) {\n",
       "      __pyx_v_out = ((PyArrayObject *)Py_None); __Pyx_INCREF(Py_None); __pyx_pybuffernd_out.rcbuffer->pybuffer.buf = NULL;\n",
       "      __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "    } else {__pyx_pybuffernd_out.diminfo[0].strides = __pyx_pybuffernd_out.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_out.diminfo[0].shape = __pyx_pybuffernd_out.rcbuffer->pybuffer.shape[0];\n",
       "    }\n",
       "  }\n",
       "  __pyx_t_6 = 0;\n",
       "  __pyx_v_out = ((PyArrayObject *)__pyx_t_5);\n",
       "  __pyx_t_5 = 0;\n",
       "
 18:     cdef double complex temp_sum
\n", "
 19:     cdef double complex temp_mult
\n", "
+20:     for i in range(time_vector.shape[0]):
\n", "
  __pyx_t_7 = (__pyx_v_time_vector->dimensions[0]);\n",
       "  for (__pyx_t_8 = 0; __pyx_t_8 < __pyx_t_7; __pyx_t_8+=1) {\n",
       "    __pyx_v_i = __pyx_t_8;\n",
       "
+21:         temp_sum = 0
\n", "
    __pyx_v_temp_sum = __pyx_t_double_complex_from_parts(0, 0);\n",
       "
+22:         temp_mult = exp(I * time_vector[i] * omega)
\n", "
    __pyx_t_9 = __pyx_v_i;\n",
       "    __pyx_t_10 = __Pyx_c_prod_npy_float64(__Pyx_c_prod_npy_float64(__pyx_t_npy_float64_complex_from_parts(__Pyx_CREAL(__pyx_v_46_cython_magic_59850e01233c87321a5c3910c7be7295_I), __Pyx_CIMAG(__pyx_v_46_cython_magic_59850e01233c87321a5c3910c7be7295_I)), __pyx_t_npy_float64_complex_from_parts((*__Pyx_BufPtrStrided1d(__pyx_t_5numpy_float64_t *, __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.buf, __pyx_t_9, __pyx_pybuffernd_time_vector.diminfo[0].strides)), 0)), __pyx_t_npy_float64_complex_from_parts(__pyx_v_omega, 0));\n",
       "    __pyx_v_temp_mult = std::exp(__pyx_t_double_complex_from_parts(__Pyx_CREAL(__pyx_t_10), __Pyx_CIMAG(__pyx_t_10)));\n",
       "
+23:         for j in range(weights.shape[0]):
\n", "
    __pyx_t_11 = (__pyx_v_weights->dimensions[0]);\n",
       "    for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n",
       "      __pyx_v_j = __pyx_t_12;\n",
       "
+24:             temp_sum += weights[j] * temp_mult
\n", "
      __pyx_t_13 = __pyx_v_j;\n",
       "      __pyx_v_temp_sum = __Pyx_c_sum_double(__pyx_v_temp_sum, __Pyx_c_prod_double((*__Pyx_BufPtrStrided1d(__pyx_t_double_complex *, __pyx_pybuffernd_weights.rcbuffer->pybuffer.buf, __pyx_t_13, __pyx_pybuffernd_weights.diminfo[0].strides)), __pyx_v_temp_mult));\n",
       "    }\n",
       "
+25:         out[i] = real(temp_sum)
\n", "
    __pyx_t_14 = __pyx_v_i;\n",
       "    *__Pyx_BufPtrStrided1d(__pyx_t_5numpy_float64_t *, __pyx_pybuffernd_out.rcbuffer->pybuffer.buf, __pyx_t_14, __pyx_pybuffernd_out.diminfo[0].strides) = std::real(__pyx_v_temp_sum);\n",
       "  }\n",
       "
+26:     return out
\n", "
  __Pyx_XDECREF(__pyx_r);\n",
       "  __Pyx_INCREF(((PyObject *)__pyx_v_out));\n",
       "  __pyx_r = ((PyObject *)__pyx_v_out);\n",
       "  goto __pyx_L0;\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a --cplus\n", "cimport numpy as np\n", "import numpy as np\n", "import cython\n", "\n", "cdef extern from \"\" namespace \"std\" nogil:\n", " double complex exp(double complex z)\n", " double real(double complex z)\n", "\n", "cdef double complex I = 1j \n", " \n", "@cython.boundscheck(False)\n", "@cython.wraparound(False)\n", "def synthesis_cython2(np.ndarray [np.complex128_t, ndim=1] weights,\n", " double omega,\n", " np.ndarray [np.float64_t, ndim=1] time_vector):\n", " cdef int i, j\n", " cdef np.ndarray [np.float64_t, ndim=1] out = np.empty_like(time_vector, dtype=np.float)\n", " cdef double complex temp_sum\n", " cdef double complex temp_mult\n", " for i in range(time_vector.shape[0]):\n", " temp_sum = 0\n", " temp_mult = exp(I * time_vector[i] * omega)\n", " for j in range(weights.shape[0]):\n", " temp_sum += weights[j] * temp_mult\n", " out[i] = real(temp_sum)\n", " return out" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's check it's correct again:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "out = synthesis_cython2(weights, omega, time_vector)" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.allclose(ref_result, out)" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "29.7 ms ± 573 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%timeit synthesis(weights, omega, time_vector)" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "27.6 ms ± 392 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%timeit synthesis_cython2(weights, omega, time_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nice! Moving the computation of the complex exponent outside of the loop helped us match NumPy. As you can see above however, line 22 is still yellow, meaning there is some Python interaction going on. Can we eliminate that?" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_9686ecbb26e36aaeeb31076454f5407d.pyx\n", " \n", " \n", "\n", "\n", "

Generated by Cython 0.25.2

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
+01: cimport numpy as np
\n", "
  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
+02: import numpy as np
\n", "
  __pyx_t_1 = __Pyx_Import(__pyx_n_s_numpy, 0, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_np, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 03: import cython
\n", "
 04: 
\n", "
 05: cdef extern from "<complex.h>" namespace "std" nogil:
\n", "
 06:     double complex exp(double complex z)
\n", "
 07:     double real(double complex z)
\n", "
 08: 
\n", "
+09: cdef double complex I = 1j
\n", "
  __pyx_v_46_cython_magic_9686ecbb26e36aaeeb31076454f5407d_I = __pyx_t_double_complex_from_parts(0, 1.0);\n",
       "
 10: 
\n", "
 11: @cython.boundscheck(False)
\n", "
 12: @cython.wraparound(False)
\n", "
+13: def synthesis_cython3(np.ndarray [complex, ndim=1] weights,
\n", "
/* Python wrapper */\n",
       "static PyObject *__pyx_pw_46_cython_magic_9686ecbb26e36aaeeb31076454f5407d_1synthesis_cython3(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/\n",
       "static PyMethodDef __pyx_mdef_46_cython_magic_9686ecbb26e36aaeeb31076454f5407d_1synthesis_cython3 = {\"synthesis_cython3\", (PyCFunction)__pyx_pw_46_cython_magic_9686ecbb26e36aaeeb31076454f5407d_1synthesis_cython3, METH_VARARGS|METH_KEYWORDS, 0};\n",
       "static PyObject *__pyx_pw_46_cython_magic_9686ecbb26e36aaeeb31076454f5407d_1synthesis_cython3(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {\n",
       "  PyArrayObject *__pyx_v_weights = 0;\n",
       "  double __pyx_v_omega;\n",
       "  PyArrayObject *__pyx_v_time_vector = 0;\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"synthesis_cython3 (wrapper)\", 0);\n",
       "  {\n",
       "    static PyObject **__pyx_pyargnames[] = {&__pyx_n_s_weights,&__pyx_n_s_omega,&__pyx_n_s_time_vector,0};\n",
       "    PyObject* values[3] = {0,0,0};\n",
       "    if (unlikely(__pyx_kwds)) {\n",
       "      Py_ssize_t kw_args;\n",
       "      const Py_ssize_t pos_args = PyTuple_GET_SIZE(__pyx_args);\n",
       "      switch (pos_args) {\n",
       "        case  3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);\n",
       "        case  2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "        case  1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "        case  0: break;\n",
       "        default: goto __pyx_L5_argtuple_error;\n",
       "      }\n",
       "      kw_args = PyDict_Size(__pyx_kwds);\n",
       "      switch (pos_args) {\n",
       "        case  0:\n",
       "        if (likely((values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_weights)) != 0)) kw_args--;\n",
       "        else goto __pyx_L5_argtuple_error;\n",
       "        case  1:\n",
       "        if (likely((values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_omega)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"synthesis_cython3\", 1, 3, 3, 1); __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "        }\n",
       "        case  2:\n",
       "        if (likely((values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_time_vector)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"synthesis_cython3\", 1, 3, 3, 2); __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "        }\n",
       "      }\n",
       "      if (unlikely(kw_args > 0)) {\n",
       "        if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, pos_args, \"synthesis_cython3\") < 0)) __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "      }\n",
       "    } else if (PyTuple_GET_SIZE(__pyx_args) != 3) {\n",
       "      goto __pyx_L5_argtuple_error;\n",
       "    } else {\n",
       "      values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "      values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "      values[2] = PyTuple_GET_ITEM(__pyx_args, 2);\n",
       "    }\n",
       "    __pyx_v_weights = ((PyArrayObject *)values[0]);\n",
       "    __pyx_v_omega = __pyx_PyFloat_AsDouble(values[1]); if (unlikely((__pyx_v_omega == (double)-1) && PyErr_Occurred())) __PYX_ERR(0, 14, __pyx_L3_error)\n",
       "    __pyx_v_time_vector = ((PyArrayObject *)values[2]);\n",
       "  }\n",
       "  goto __pyx_L4_argument_unpacking_done;\n",
       "  __pyx_L5_argtuple_error:;\n",
       "  __Pyx_RaiseArgtupleInvalid(\"synthesis_cython3\", 1, 3, 3, PyTuple_GET_SIZE(__pyx_args)); __PYX_ERR(0, 13, __pyx_L3_error)\n",
       "  __pyx_L3_error:;\n",
       "  __Pyx_AddTraceback(\"_cython_magic_9686ecbb26e36aaeeb31076454f5407d.synthesis_cython3\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return NULL;\n",
       "  __pyx_L4_argument_unpacking_done:;\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_weights), __pyx_ptype_5numpy_ndarray, 1, \"weights\", 0))) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_time_vector), __pyx_ptype_5numpy_ndarray, 1, \"time_vector\", 0))) __PYX_ERR(0, 15, __pyx_L1_error)\n",
       "  __pyx_r = __pyx_pf_46_cython_magic_9686ecbb26e36aaeeb31076454f5407d_synthesis_cython3(__pyx_self, __pyx_v_weights, __pyx_v_omega, __pyx_v_time_vector);\n",
       "\n",
       "  /* function exit code */\n",
       "  goto __pyx_L0;\n",
       "  __pyx_L1_error:;\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_46_cython_magic_9686ecbb26e36aaeeb31076454f5407d_synthesis_cython3(CYTHON_UNUSED PyObject *__pyx_self, PyArrayObject *__pyx_v_weights, double __pyx_v_omega, PyArrayObject *__pyx_v_time_vector) {\n",
       "  int __pyx_v_i;\n",
       "  int __pyx_v_j;\n",
       "  PyArrayObject *__pyx_v_out = 0;\n",
       "  __pyx_t_double_complex __pyx_v_temp_sum;\n",
       "  __pyx_t_double_complex __pyx_v_temp_mult;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_out;\n",
       "  __Pyx_Buffer __pyx_pybuffer_out;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_time_vector;\n",
       "  __Pyx_Buffer __pyx_pybuffer_time_vector;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_weights;\n",
       "  __Pyx_Buffer __pyx_pybuffer_weights;\n",
       "  PyObject *__pyx_r = NULL;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"synthesis_cython3\", 0);\n",
       "  __pyx_pybuffer_out.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_out.refcount = 0;\n",
       "  __pyx_pybuffernd_out.data = NULL;\n",
       "  __pyx_pybuffernd_out.rcbuffer = &__pyx_pybuffer_out;\n",
       "  __pyx_pybuffer_weights.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_weights.refcount = 0;\n",
       "  __pyx_pybuffernd_weights.data = NULL;\n",
       "  __pyx_pybuffernd_weights.rcbuffer = &__pyx_pybuffer_weights;\n",
       "  __pyx_pybuffer_time_vector.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_time_vector.refcount = 0;\n",
       "  __pyx_pybuffernd_time_vector.data = NULL;\n",
       "  __pyx_pybuffernd_time_vector.rcbuffer = &__pyx_pybuffer_time_vector;\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_weights.rcbuffer->pybuffer, (PyObject*)__pyx_v_weights, &__Pyx_TypeInfo___pyx_t_double_complex, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_weights.diminfo[0].strides = __pyx_pybuffernd_weights.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_weights.diminfo[0].shape = __pyx_pybuffernd_weights.rcbuffer->pybuffer.shape[0];\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer, (PyObject*)__pyx_v_time_vector, &__Pyx_TypeInfo_double, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_time_vector.diminfo[0].strides = __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_time_vector.diminfo[0].shape = __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.shape[0];\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_L1_error:;\n",
       "  __Pyx_XDECREF(__pyx_t_1);\n",
       "  __Pyx_XDECREF(__pyx_t_2);\n",
       "  __Pyx_XDECREF(__pyx_t_3);\n",
       "  __Pyx_XDECREF(__pyx_t_4);\n",
       "  __Pyx_XDECREF(__pyx_t_5);\n",
       "  { PyObject *__pyx_type, *__pyx_value, *__pyx_tb;\n",
       "    __Pyx_PyThreadState_declare\n",
       "    __Pyx_PyThreadState_assign\n",
       "    __Pyx_ErrFetch(&__pyx_type, &__pyx_value, &__pyx_tb);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __Pyx_ErrRestore(__pyx_type, __pyx_value, __pyx_tb);}\n",
       "  __Pyx_AddTraceback(\"_cython_magic_9686ecbb26e36aaeeb31076454f5407d.synthesis_cython3\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  goto __pyx_L2;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __pyx_L2:;\n",
       "  __Pyx_XDECREF((PyObject *)__pyx_v_out);\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "/* … */\n",
       "  __pyx_tuple__10 = PyTuple_Pack(8, __pyx_n_s_weights, __pyx_n_s_omega, __pyx_n_s_time_vector, __pyx_n_s_i, __pyx_n_s_j, __pyx_n_s_out, __pyx_n_s_temp_sum, __pyx_n_s_temp_mult); if (unlikely(!__pyx_tuple__10)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_tuple__10);\n",
       "  __Pyx_GIVEREF(__pyx_tuple__10);\n",
       "/* … */\n",
       "  __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_46_cython_magic_9686ecbb26e36aaeeb31076454f5407d_1synthesis_cython3, NULL, __pyx_n_s_cython_magic_9686ecbb26e36aaeeb); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_synthesis_cython3, __pyx_t_1) < 0) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 14:                      double omega,
\n", "
 15:                      np.ndarray [double, ndim=1] time_vector):
\n", "
 16:     cdef int i, j
\n", "
+17:     cdef np.ndarray [np.float64_t, ndim=1] out = np.empty_like(time_vector, dtype=np.float)
\n", "
  __pyx_t_1 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __pyx_t_2 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_empty_like); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_2);\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __pyx_t_1 = PyTuple_New(1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __Pyx_INCREF(((PyObject *)__pyx_v_time_vector));\n",
       "  __Pyx_GIVEREF(((PyObject *)__pyx_v_time_vector));\n",
       "  PyTuple_SET_ITEM(__pyx_t_1, 0, ((PyObject *)__pyx_v_time_vector));\n",
       "  __pyx_t_3 = PyDict_New(); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __pyx_t_4 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __pyx_t_5 = __Pyx_PyObject_GetAttrStr(__pyx_t_4, __pyx_n_s_float); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "  if (PyDict_SetItem(__pyx_t_3, __pyx_n_s_dtype, __pyx_t_5) < 0) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;\n",
       "  __pyx_t_5 = __Pyx_PyObject_Call(__pyx_t_2, __pyx_t_1, __pyx_t_3); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;\n",
       "  if (!(likely(((__pyx_t_5) == Py_None) || likely(__Pyx_TypeTest(__pyx_t_5, __pyx_ptype_5numpy_ndarray))))) __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "  __pyx_t_6 = ((PyArrayObject *)__pyx_t_5);\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_out.rcbuffer->pybuffer, (PyObject*)__pyx_t_6, &__Pyx_TypeInfo_nn___pyx_t_5numpy_float64_t, PyBUF_FORMAT| PyBUF_STRIDES| PyBUF_WRITABLE, 1, 0, __pyx_stack) == -1)) {\n",
       "      __pyx_v_out = ((PyArrayObject *)Py_None); __Pyx_INCREF(Py_None); __pyx_pybuffernd_out.rcbuffer->pybuffer.buf = NULL;\n",
       "      __PYX_ERR(0, 17, __pyx_L1_error)\n",
       "    } else {__pyx_pybuffernd_out.diminfo[0].strides = __pyx_pybuffernd_out.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_out.diminfo[0].shape = __pyx_pybuffernd_out.rcbuffer->pybuffer.shape[0];\n",
       "    }\n",
       "  }\n",
       "  __pyx_t_6 = 0;\n",
       "  __pyx_v_out = ((PyArrayObject *)__pyx_t_5);\n",
       "  __pyx_t_5 = 0;\n",
       "
 18:     cdef double complex temp_sum
\n", "
 19:     cdef double complex temp_mult
\n", "
+20:     for i in range(time_vector.shape[0]):
\n", "
  __pyx_t_7 = (__pyx_v_time_vector->dimensions[0]);\n",
       "  for (__pyx_t_8 = 0; __pyx_t_8 < __pyx_t_7; __pyx_t_8+=1) {\n",
       "    __pyx_v_i = __pyx_t_8;\n",
       "
+21:         temp_sum = 0
\n", "
    __pyx_v_temp_sum = __pyx_t_double_complex_from_parts(0, 0);\n",
       "
+22:         temp_mult = exp(I * time_vector[i] * omega)
\n", "
    __pyx_t_9 = __pyx_v_i;\n",
       "    __pyx_v_temp_mult = std::exp(__Pyx_c_prod_double(__Pyx_c_prod_double(__pyx_v_46_cython_magic_9686ecbb26e36aaeeb31076454f5407d_I, __pyx_t_double_complex_from_parts((*__Pyx_BufPtrStrided1d(double *, __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.buf, __pyx_t_9, __pyx_pybuffernd_time_vector.diminfo[0].strides)), 0)), __pyx_t_double_complex_from_parts(__pyx_v_omega, 0)));\n",
       "
+23:         for j in range(weights.shape[0]):
\n", "
    __pyx_t_10 = (__pyx_v_weights->dimensions[0]);\n",
       "    for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n",
       "      __pyx_v_j = __pyx_t_11;\n",
       "
+24:             temp_sum += weights[j] * temp_mult
\n", "
      __pyx_t_12 = __pyx_v_j;\n",
       "      __pyx_v_temp_sum = __Pyx_c_sum_double(__pyx_v_temp_sum, __Pyx_c_prod_double((*__Pyx_BufPtrStrided1d(__pyx_t_double_complex *, __pyx_pybuffernd_weights.rcbuffer->pybuffer.buf, __pyx_t_12, __pyx_pybuffernd_weights.diminfo[0].strides)), __pyx_v_temp_mult));\n",
       "    }\n",
       "
+25:         out[i] = real(temp_sum)
\n", "
    __pyx_t_13 = __pyx_v_i;\n",
       "    *__Pyx_BufPtrStrided1d(__pyx_t_5numpy_float64_t *, __pyx_pybuffernd_out.rcbuffer->pybuffer.buf, __pyx_t_13, __pyx_pybuffernd_out.diminfo[0].strides) = std::real(__pyx_v_temp_sum);\n",
       "  }\n",
       "
+26:     return out
\n", "
  __Pyx_XDECREF(__pyx_r);\n",
       "  __Pyx_INCREF(((PyObject *)__pyx_v_out));\n",
       "  __pyx_r = ((PyObject *)__pyx_v_out);\n",
       "  goto __pyx_L0;\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a --cplus\n", "cimport numpy as np\n", "import numpy as np\n", "import cython\n", "\n", "cdef extern from \"\" namespace \"std\" nogil:\n", " double complex exp(double complex z)\n", " double real(double complex z)\n", "\n", "cdef double complex I = 1j \n", " \n", "@cython.boundscheck(False)\n", "@cython.wraparound(False)\n", "def synthesis_cython3(np.ndarray [complex, ndim=1] weights,\n", " double omega,\n", " np.ndarray [double, ndim=1] time_vector):\n", " cdef int i, j\n", " cdef np.ndarray [np.float64_t, ndim=1] out = np.empty_like(time_vector, dtype=np.float)\n", " cdef double complex temp_sum\n", " cdef double complex temp_mult\n", " for i in range(time_vector.shape[0]):\n", " temp_sum = 0\n", " temp_mult = exp(I * time_vector[i] * omega)\n", " for j in range(weights.shape[0]):\n", " temp_sum += weights[j] * temp_mult\n", " out[i] = real(temp_sum)\n", " return out" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Yes we can! All we did was changing the dtype of the input array. Let's check our function produces correct results again:" ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "collapsed": true }, "outputs": [], "source": [ "out = synthesis_cython3(weights, omega, time_vector)" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.allclose(ref_result, out)" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "29.9 ms ± 1.36 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%timeit synthesis(weights, omega, time_vector)" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "28.1 ms ± 1.36 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%timeit synthesis_cython3(weights, omega, time_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Interestingly, even though our function is now \"completely white\", meaning that Cython has translated it to pure C++, it is not actually faster than our previous version. The last thing we can do in our quest for performance is to make the loop parallel." ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_e7cc766498993d3947c8c5bbc875e6a8.pyx\n", " \n", " \n", "\n", "\n", "

Generated by Cython 0.25.2

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
+01: cimport numpy as np
\n", "
  __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
+02: import numpy as np
\n", "
  __pyx_t_1 = __Pyx_Import(__pyx_n_s_numpy, 0, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_np, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 03: import cython
\n", "
 04: from cython.parallel import prange
\n", "
 05: 
\n", "
 06: cdef extern from "<complex.h>" namespace "std" nogil:
\n", "
 07:     double complex exp(double complex z)
\n", "
 08:     double real(double complex z)
\n", "
 09: 
\n", "
+10: cdef double complex I = 1j
\n", "
  __pyx_v_46_cython_magic_e7cc766498993d3947c8c5bbc875e6a8_I = __pyx_t_double_complex_from_parts(0, 1.0);\n",
       "
 11: 
\n", "
 12: @cython.boundscheck(False)
\n", "
 13: @cython.wraparound(False)
\n", "
+14: def synthesis_cython4(np.ndarray [complex, ndim=1] weights,
\n", "
/* Python wrapper */\n",
       "static PyObject *__pyx_pw_46_cython_magic_e7cc766498993d3947c8c5bbc875e6a8_1synthesis_cython4(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/\n",
       "static PyMethodDef __pyx_mdef_46_cython_magic_e7cc766498993d3947c8c5bbc875e6a8_1synthesis_cython4 = {\"synthesis_cython4\", (PyCFunction)__pyx_pw_46_cython_magic_e7cc766498993d3947c8c5bbc875e6a8_1synthesis_cython4, METH_VARARGS|METH_KEYWORDS, 0};\n",
       "static PyObject *__pyx_pw_46_cython_magic_e7cc766498993d3947c8c5bbc875e6a8_1synthesis_cython4(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {\n",
       "  PyArrayObject *__pyx_v_weights = 0;\n",
       "  double __pyx_v_omega;\n",
       "  PyArrayObject *__pyx_v_time_vector = 0;\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"synthesis_cython4 (wrapper)\", 0);\n",
       "  {\n",
       "    static PyObject **__pyx_pyargnames[] = {&__pyx_n_s_weights,&__pyx_n_s_omega,&__pyx_n_s_time_vector,0};\n",
       "    PyObject* values[3] = {0,0,0};\n",
       "    if (unlikely(__pyx_kwds)) {\n",
       "      Py_ssize_t kw_args;\n",
       "      const Py_ssize_t pos_args = PyTuple_GET_SIZE(__pyx_args);\n",
       "      switch (pos_args) {\n",
       "        case  3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);\n",
       "        case  2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "        case  1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "        case  0: break;\n",
       "        default: goto __pyx_L5_argtuple_error;\n",
       "      }\n",
       "      kw_args = PyDict_Size(__pyx_kwds);\n",
       "      switch (pos_args) {\n",
       "        case  0:\n",
       "        if (likely((values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_weights)) != 0)) kw_args--;\n",
       "        else goto __pyx_L5_argtuple_error;\n",
       "        case  1:\n",
       "        if (likely((values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_omega)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"synthesis_cython4\", 1, 3, 3, 1); __PYX_ERR(0, 14, __pyx_L3_error)\n",
       "        }\n",
       "        case  2:\n",
       "        if (likely((values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s_time_vector)) != 0)) kw_args--;\n",
       "        else {\n",
       "          __Pyx_RaiseArgtupleInvalid(\"synthesis_cython4\", 1, 3, 3, 2); __PYX_ERR(0, 14, __pyx_L3_error)\n",
       "        }\n",
       "      }\n",
       "      if (unlikely(kw_args > 0)) {\n",
       "        if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, pos_args, \"synthesis_cython4\") < 0)) __PYX_ERR(0, 14, __pyx_L3_error)\n",
       "      }\n",
       "    } else if (PyTuple_GET_SIZE(__pyx_args) != 3) {\n",
       "      goto __pyx_L5_argtuple_error;\n",
       "    } else {\n",
       "      values[0] = PyTuple_GET_ITEM(__pyx_args, 0);\n",
       "      values[1] = PyTuple_GET_ITEM(__pyx_args, 1);\n",
       "      values[2] = PyTuple_GET_ITEM(__pyx_args, 2);\n",
       "    }\n",
       "    __pyx_v_weights = ((PyArrayObject *)values[0]);\n",
       "    __pyx_v_omega = __pyx_PyFloat_AsDouble(values[1]); if (unlikely((__pyx_v_omega == (double)-1) && PyErr_Occurred())) __PYX_ERR(0, 15, __pyx_L3_error)\n",
       "    __pyx_v_time_vector = ((PyArrayObject *)values[2]);\n",
       "  }\n",
       "  goto __pyx_L4_argument_unpacking_done;\n",
       "  __pyx_L5_argtuple_error:;\n",
       "  __Pyx_RaiseArgtupleInvalid(\"synthesis_cython4\", 1, 3, 3, PyTuple_GET_SIZE(__pyx_args)); __PYX_ERR(0, 14, __pyx_L3_error)\n",
       "  __pyx_L3_error:;\n",
       "  __Pyx_AddTraceback(\"_cython_magic_e7cc766498993d3947c8c5bbc875e6a8.synthesis_cython4\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return NULL;\n",
       "  __pyx_L4_argument_unpacking_done:;\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_weights), __pyx_ptype_5numpy_ndarray, 1, \"weights\", 0))) __PYX_ERR(0, 14, __pyx_L1_error)\n",
       "  if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_time_vector), __pyx_ptype_5numpy_ndarray, 1, \"time_vector\", 0))) __PYX_ERR(0, 16, __pyx_L1_error)\n",
       "  __pyx_r = __pyx_pf_46_cython_magic_e7cc766498993d3947c8c5bbc875e6a8_synthesis_cython4(__pyx_self, __pyx_v_weights, __pyx_v_omega, __pyx_v_time_vector);\n",
       "\n",
       "  /* function exit code */\n",
       "  goto __pyx_L0;\n",
       "  __pyx_L1_error:;\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_46_cython_magic_e7cc766498993d3947c8c5bbc875e6a8_synthesis_cython4(CYTHON_UNUSED PyObject *__pyx_self, PyArrayObject *__pyx_v_weights, double __pyx_v_omega, PyArrayObject *__pyx_v_time_vector) {\n",
       "  int __pyx_v_i;\n",
       "  int __pyx_v_j;\n",
       "  PyArrayObject *__pyx_v_out = 0;\n",
       "  __pyx_t_double_complex __pyx_v_temp_sum;\n",
       "  __pyx_t_double_complex __pyx_v_temp_mult;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_out;\n",
       "  __Pyx_Buffer __pyx_pybuffer_out;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_time_vector;\n",
       "  __Pyx_Buffer __pyx_pybuffer_time_vector;\n",
       "  __Pyx_LocalBuf_ND __pyx_pybuffernd_weights;\n",
       "  __Pyx_Buffer __pyx_pybuffer_weights;\n",
       "  PyObject *__pyx_r = NULL;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"synthesis_cython4\", 0);\n",
       "  __pyx_pybuffer_out.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_out.refcount = 0;\n",
       "  __pyx_pybuffernd_out.data = NULL;\n",
       "  __pyx_pybuffernd_out.rcbuffer = &__pyx_pybuffer_out;\n",
       "  __pyx_pybuffer_weights.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_weights.refcount = 0;\n",
       "  __pyx_pybuffernd_weights.data = NULL;\n",
       "  __pyx_pybuffernd_weights.rcbuffer = &__pyx_pybuffer_weights;\n",
       "  __pyx_pybuffer_time_vector.pybuffer.buf = NULL;\n",
       "  __pyx_pybuffer_time_vector.refcount = 0;\n",
       "  __pyx_pybuffernd_time_vector.data = NULL;\n",
       "  __pyx_pybuffernd_time_vector.rcbuffer = &__pyx_pybuffer_time_vector;\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_weights.rcbuffer->pybuffer, (PyObject*)__pyx_v_weights, &__Pyx_TypeInfo___pyx_t_double_complex, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 14, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_weights.diminfo[0].strides = __pyx_pybuffernd_weights.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_weights.diminfo[0].shape = __pyx_pybuffernd_weights.rcbuffer->pybuffer.shape[0];\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer, (PyObject*)__pyx_v_time_vector, &__Pyx_TypeInfo_double, PyBUF_FORMAT| PyBUF_STRIDES, 1, 0, __pyx_stack) == -1)) __PYX_ERR(0, 14, __pyx_L1_error)\n",
       "  }\n",
       "  __pyx_pybuffernd_time_vector.diminfo[0].strides = __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_time_vector.diminfo[0].shape = __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.shape[0];\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_L1_error:;\n",
       "  __Pyx_XDECREF(__pyx_t_1);\n",
       "  __Pyx_XDECREF(__pyx_t_2);\n",
       "  __Pyx_XDECREF(__pyx_t_3);\n",
       "  __Pyx_XDECREF(__pyx_t_4);\n",
       "  __Pyx_XDECREF(__pyx_t_5);\n",
       "  { PyObject *__pyx_type, *__pyx_value, *__pyx_tb;\n",
       "    __Pyx_PyThreadState_declare\n",
       "    __Pyx_PyThreadState_assign\n",
       "    __Pyx_ErrFetch(&__pyx_type, &__pyx_value, &__pyx_tb);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer);\n",
       "    __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __Pyx_ErrRestore(__pyx_type, __pyx_value, __pyx_tb);}\n",
       "  __Pyx_AddTraceback(\"_cython_magic_e7cc766498993d3947c8c5bbc875e6a8.synthesis_cython4\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  goto __pyx_L2;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_out.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_time_vector.rcbuffer->pybuffer);\n",
       "  __Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_weights.rcbuffer->pybuffer);\n",
       "  __pyx_L2:;\n",
       "  __Pyx_XDECREF((PyObject *)__pyx_v_out);\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "/* … */\n",
       "  __pyx_tuple__10 = PyTuple_Pack(8, __pyx_n_s_weights, __pyx_n_s_omega, __pyx_n_s_time_vector, __pyx_n_s_i, __pyx_n_s_j, __pyx_n_s_out, __pyx_n_s_temp_sum, __pyx_n_s_temp_mult); if (unlikely(!__pyx_tuple__10)) __PYX_ERR(0, 14, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_tuple__10);\n",
       "  __Pyx_GIVEREF(__pyx_tuple__10);\n",
       "/* … */\n",
       "  __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_46_cython_magic_e7cc766498993d3947c8c5bbc875e6a8_1synthesis_cython4, NULL, __pyx_n_s_cython_magic_e7cc766498993d3947); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 14, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_synthesis_cython4, __pyx_t_1) < 0) __PYX_ERR(0, 14, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "
 15:                      double omega,
\n", "
 16:                      np.ndarray [double, ndim=1] time_vector):
\n", "
 17:     cdef int i, j
\n", "
+18:     cdef np.ndarray [np.float64_t, ndim=1] out = np.empty_like(time_vector, dtype=np.float)
\n", "
  __pyx_t_1 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __pyx_t_2 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_empty_like); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_2);\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __pyx_t_1 = PyTuple_New(1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __Pyx_INCREF(((PyObject *)__pyx_v_time_vector));\n",
       "  __Pyx_GIVEREF(((PyObject *)__pyx_v_time_vector));\n",
       "  PyTuple_SET_ITEM(__pyx_t_1, 0, ((PyObject *)__pyx_v_time_vector));\n",
       "  __pyx_t_3 = PyDict_New(); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __pyx_t_4 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __pyx_t_5 = __Pyx_PyObject_GetAttrStr(__pyx_t_4, __pyx_n_s_float); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "  if (PyDict_SetItem(__pyx_t_3, __pyx_n_s_dtype, __pyx_t_5) < 0) __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;\n",
       "  __pyx_t_5 = __Pyx_PyObject_Call(__pyx_t_2, __pyx_t_1, __pyx_t_3); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;\n",
       "  if (!(likely(((__pyx_t_5) == Py_None) || likely(__Pyx_TypeTest(__pyx_t_5, __pyx_ptype_5numpy_ndarray))))) __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "  __pyx_t_6 = ((PyArrayObject *)__pyx_t_5);\n",
       "  {\n",
       "    __Pyx_BufFmt_StackElem __pyx_stack[1];\n",
       "    if (unlikely(__Pyx_GetBufferAndValidate(&__pyx_pybuffernd_out.rcbuffer->pybuffer, (PyObject*)__pyx_t_6, &__Pyx_TypeInfo_nn___pyx_t_5numpy_float64_t, PyBUF_FORMAT| PyBUF_STRIDES| PyBUF_WRITABLE, 1, 0, __pyx_stack) == -1)) {\n",
       "      __pyx_v_out = ((PyArrayObject *)Py_None); __Pyx_INCREF(Py_None); __pyx_pybuffernd_out.rcbuffer->pybuffer.buf = NULL;\n",
       "      __PYX_ERR(0, 18, __pyx_L1_error)\n",
       "    } else {__pyx_pybuffernd_out.diminfo[0].strides = __pyx_pybuffernd_out.rcbuffer->pybuffer.strides[0]; __pyx_pybuffernd_out.diminfo[0].shape = __pyx_pybuffernd_out.rcbuffer->pybuffer.shape[0];\n",
       "    }\n",
       "  }\n",
       "  __pyx_t_6 = 0;\n",
       "  __pyx_v_out = ((PyArrayObject *)__pyx_t_5);\n",
       "  __pyx_t_5 = 0;\n",
       "
 19:     cdef double complex temp_sum
\n", "
 20:     cdef double complex temp_mult
\n", "
+21:     for i in prange(time_vector.shape[0], nogil=True):
\n", "
  {\n",
       "      #ifdef WITH_THREAD\n",
       "      PyThreadState *_save;\n",
       "      Py_UNBLOCK_THREADS\n",
       "      #endif\n",
       "      /*try:*/ {\n",
       "        __pyx_t_7 = (__pyx_v_time_vector->dimensions[0]);\n",
       "        if (1 == 0) abort();\n",
       "        {\n",
       "            #if ((defined(__APPLE__) || defined(__OSX__)) && (defined(__GNUC__) && (__GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95)))))\n",
       "                #undef likely\n",
       "                #undef unlikely\n",
       "                #define likely(x)   (x)\n",
       "                #define unlikely(x) (x)\n",
       "            #endif\n",
       "            __pyx_t_9 = (__pyx_t_7 - 0 + 1 - 1/abs(1)) / 1;\n",
       "            if (__pyx_t_9 > 0)\n",
       "            {\n",
       "                #ifdef _OPENMP\n",
       "                #pragma omp parallel\n",
       "                #endif /* _OPENMP */\n",
       "                {\n",
       "                    #ifdef _OPENMP\n",
       "                    #pragma omp for firstprivate(__pyx_v_i) lastprivate(__pyx_v_i) lastprivate(__pyx_v_j) lastprivate(__pyx_v_temp_mult) lastprivate(__pyx_v_temp_sum)\n",
       "                    #endif /* _OPENMP */\n",
       "                    for (__pyx_t_8 = 0; __pyx_t_8 < __pyx_t_9; __pyx_t_8++){\n",
       "                        {\n",
       "                            __pyx_v_i = (int)(0 + 1 * __pyx_t_8);\n",
       "                            /* Initialize private variables to invalid values */\n",
       "                            __pyx_v_j = ((int)0xbad0bad0);\n",
       "/* … */\n",
       "      /*finally:*/ {\n",
       "        /*normal exit:*/{\n",
       "          #ifdef WITH_THREAD\n",
       "          Py_BLOCK_THREADS\n",
       "          #endif\n",
       "          goto __pyx_L5;\n",
       "        }\n",
       "        __pyx_L5:;\n",
       "      }\n",
       "  }\n",
       "
+22:         temp_sum = 0
\n", "
                            __pyx_v_temp_sum = __pyx_t_double_complex_from_parts(0, 0);\n",
       "
+23:         temp_mult = exp(I * time_vector[i] * omega)
\n", "
                            __pyx_t_10 = __pyx_v_i;\n",
       "                            __pyx_v_temp_mult = std::exp(__Pyx_c_prod_double(__Pyx_c_prod_double(__pyx_v_46_cython_magic_e7cc766498993d3947c8c5bbc875e6a8_I, __pyx_t_double_complex_from_parts((*__Pyx_BufPtrStrided1d(double *, __pyx_pybuffernd_time_vector.rcbuffer->pybuffer.buf, __pyx_t_10, __pyx_pybuffernd_time_vector.diminfo[0].strides)), 0)), __pyx_t_double_complex_from_parts(__pyx_v_omega, 0)));\n",
       "
+24:         for j in range(weights.shape[0]):
\n", "
                            __pyx_t_11 = (__pyx_v_weights->dimensions[0]);\n",
       "                            for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n",
       "                              __pyx_v_j = __pyx_t_12;\n",
       "
+25:             temp_sum = temp_sum + weights[j] * temp_mult
\n", "
                              __pyx_t_13 = __pyx_v_j;\n",
       "                              __pyx_v_temp_sum = __Pyx_c_sum_double(__pyx_v_temp_sum, __Pyx_c_prod_double((*__Pyx_BufPtrStrided1d(__pyx_t_double_complex *, __pyx_pybuffernd_weights.rcbuffer->pybuffer.buf, __pyx_t_13, __pyx_pybuffernd_weights.diminfo[0].strides)), __pyx_v_temp_mult));\n",
       "                            }\n",
       "
+26:         out[i] = real(temp_sum)
\n", "
                            __pyx_t_14 = __pyx_v_i;\n",
       "                            *__Pyx_BufPtrStrided1d(__pyx_t_5numpy_float64_t *, __pyx_pybuffernd_out.rcbuffer->pybuffer.buf, __pyx_t_14, __pyx_pybuffernd_out.diminfo[0].strides) = std::real(__pyx_v_temp_sum);\n",
       "                        }\n",
       "                    }\n",
       "                }\n",
       "            }\n",
       "        }\n",
       "        #if ((defined(__APPLE__) || defined(__OSX__)) && (defined(__GNUC__) && (__GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95)))))\n",
       "            #undef likely\n",
       "            #undef unlikely\n",
       "            #define likely(x)   __builtin_expect(!!(x), 1)\n",
       "            #define unlikely(x) __builtin_expect(!!(x), 0)\n",
       "        #endif\n",
       "      }\n",
       "
+27:     return out
\n", "
  __Pyx_XDECREF(__pyx_r);\n",
       "  __Pyx_INCREF(((PyObject *)__pyx_v_out));\n",
       "  __pyx_r = ((PyObject *)__pyx_v_out);\n",
       "  goto __pyx_L0;\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a --cplus --compile-args=/openmp\n", "cimport numpy as np\n", "import numpy as np\n", "import cython\n", "from cython.parallel import prange\n", "\n", "cdef extern from \"\" namespace \"std\" nogil:\n", " double complex exp(double complex z)\n", " double real(double complex z)\n", "\n", "cdef double complex I = 1j \n", " \n", "@cython.boundscheck(False)\n", "@cython.wraparound(False)\n", "def synthesis_cython4(np.ndarray [complex, ndim=1] weights,\n", " double omega,\n", " np.ndarray [double, ndim=1] time_vector):\n", " cdef int i, j\n", " cdef np.ndarray [np.float64_t, ndim=1] out = np.empty_like(time_vector, dtype=np.float)\n", " cdef double complex temp_sum\n", " cdef double complex temp_mult\n", " for i in prange(time_vector.shape[0], nogil=True):\n", " temp_sum = 0\n", " temp_mult = exp(I * time_vector[i] * omega)\n", " for j in range(weights.shape[0]):\n", " temp_sum = temp_sum + weights[j] * temp_mult\n", " out[i] = real(temp_sum)\n", " return out" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that I had to give up my `+=` term to compile this since I ran into this error message \"Cannot read reduction variable in loop body\" (fix found [here](https://stackoverflow.com/questions/40451203/cython-parallel-loop-problems)).\n", "\n", "Also, note that I supplied the compiler with the *--/openmp* flag, which is valid under Windows only (for Linux it's *--fopenmp*)." ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [], "source": [ "out = synthesis_cython4(weights, omega, time_vector)" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.allclose(ref_result, out)" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "29.1 ms ± 250 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "%timeit synthesis(weights, omega, time_vector)" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "9.23 ms ± 521 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "%timeit synthesis_cython4(weights, omega, time_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We achieve a nice little speedup to parallel execution on four cores." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Timings " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's time the different versions of our functions!" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "29.3 ms ± 639 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", "123 ms ± 3.54 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", "27.5 ms ± 352 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", "27.3 ms ± 218 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", "8.76 ms ± 599 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "timings = {}\n", "for func, label in zip([synthesis, synthesis_cython1, synthesis_cython2, synthesis_cython3, synthesis_cython4],\n", " ['reference', 'cython1-naive', 'cython2-clever', 'cython3-clever-pure-c++-loop', 'cython-parallel']):\n", " obj = %timeit -o func(weights, omega, time_vector)\n", " timings[label] = obj" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
timings (ms)
cython-parallel8.757900
cython3-clever-pure-c++-loop27.341302
cython2-clever27.519439
reference29.275485
cython1-naive123.381547
\n", "
" ], "text/plain": [ " timings (ms)\n", "cython-parallel 8.757900\n", "cython3-clever-pure-c++-loop 27.341302\n", "cython2-clever 27.519439\n", "reference 29.275485\n", "cython1-naive 123.381547" ] }, "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "s = pd.Series({key: timings[key].average * 1e3 for key in timings}).to_frame(name='timings (ms)').sort_values(by='timings (ms)')\n", "s" ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlMAAAGUCAYAAAARRKd5AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3XtgXHWZ//H3k0zSpLm2SWwruK2LICAIWLyhAksRQVwR\nvOGKsoK33Qr6U1dBf4J3vO2u+/OyLoKK7qrrelkQdxUWuehaL0sBAVm5KJVq0qZpc23aSSbP749z\nksbSUjqZyckz5/P6h2bOkHlOPsnM93zP9zzH3B0RERERKU9d1gWIiIiIRKbBlIiIiMgcaDAlIiIi\nMgcaTImIiIjMgQZTIiIiInOgwZSIiIjIHBSyeuGbbrrJFy1alNXLV93k5CSFQmY/XpkDZReb8otL\n2cVW6/lt3759y5o1a3r2tC2zvV60aBGHHnpoVi9fdRs2bGDlypVZlyFlUHaxKb+4lF1stZ7f+vXr\nN+xtm07zVcny5cuzLkHKpOxiU35xKbvY8pyfBlNV0tfXl3UJUiZlF5vyi0vZxZbn/DSYqpKGhoas\nS5AyKbvYlF9cyi62POe3oFaKuTujo6PUwv0Cm5qaGB4ezrqMspgZra2tmFnWpWSio6Mj6xJkDpRf\nXMoutjznt6AGU6OjoyxatIjGxsasS5mznTt3EvVqxWKxyOjoKG1tbVmXkoktW7bQ0tKSdRlSJuUX\nl7KLLc/5LajTfO5eEwMpgPr6+qxLKFtjY2NNzA6WK89HV7VA+cWl7GLLc34LajBVS/I8GImuWCxm\nXYLMgfKLS9nFluf8NJiaZWhoiCuvvHLm697eXs4999z9+h4f/vCHuemmm5iamqp0eTMuvvhifvKT\nn8z5+5x55pkMDg5WoKLaMj4+nnUJMgfKLy5lF1ue87OsZlDWrVvnuzftHB4epr29febrU664raKv\ned1rj3nE7b/73e84++yzKzJQmZqaoq6u8mPVrVu38vKXv5zrr79+zt/ra1/7Gn/4wx9429ve9rBt\nu2eRJ5HXu4nyi0zZxVbr+a1fv/7WNWvWHLunbZqZmuV973sfDz74IMcffzyXXHIJv/vd7zjuuOMA\n+OpXv8o555zDmWeeyVFHHcXnP/95PvOZz3DCCSfw3Oc+l23btgGwdu1arr76aiYmJjjqqKO47LLL\nOPHEE3nWs57FvffeCySL9M4880ye+cxncuGFF/LkJz+ZgYEBxsbGePnLX85znvMcjjvuOL797W8/\nrMZrrrmGNWvWzHx91FFH8f73v5/jjz+ek046iTvuuIMXv/jFPOUpT+GLX/wikPT+OP300zn++OM5\n7rjjWLduHQCnnXYa3/rWt6r6M40oz71SaoHyi0vZxZbn/DSYmuXSSy9l1apV3HLLLbz//e9/2PZ7\n7rmHL3/5y9xwww188IMfZPHixdx888089alP5etf//ofPXe6rUBXVxc33XQT5513Hp/+9KcB+NjH\nPsbxxx/PunXreOELX8jGjRsBuOGGG1i+fDk/+tGP+MlPfsLJJ5/8sBp+/vOfc/TRR//RYwceeCC3\n3HILz3jGM1i7di1f+tKXuO666/jIRz4CwDe/+U1OOukkbrnlFn70ox9xxBFHANDZ2UmxWGTr1q1z\n/MnVllq5CCKvlF9cyi62POe3oFojLHTPfvazaWtro62tjfb2dp73vOcBcPjhh3P33Xf/0XOnr+Z7\nwQteACQzSNdeey0AP/3pT/nKV74CwMknn0xnZ+fM93nPe97De9/7Xp73vOfxzGc+82E19PX10dXV\n9UePnXrqqTP//9jY2EyNjY2NDA0N8ZSnPIULLriAiYkJTj/9dI488siZ/7e7u5u+vj6WLl06559P\nrchrS4haofziUnaVVemlMvuyYlGJ3p3zNzu1r6U780kzU/th9rngurq6ma/r6uoolUp/9NzJyck/\n+n/q6+tnHtubJzzhCdx0000cfvjhfOhDH+JjH/vYw57T3NzMzp0791jX7Jqmv56cnOS4447j2muv\nZcWKFaxdu/aPZtF27txJU1PTPvc9TwYGBrIuQeZA+cWl7GI7pK207yfVKA2mZmltbWV0dLQi36tQ\n2Puk39Of/nT+/d//HYAf/vCHM1fU9fb20tzczMte9jIuuOACfvnLXz7s/z3kkEP4zW9+s1+1PPTQ\nQzzmMY/h3HPP5VWvehV33HEHkLRv2Lx5M3/yJ3+yX9+v1i1ZsiTrEmQOlF9cyi62B8bi9lecq30O\npszsC2a22czumvXYx83sf83sl2b2HTPrnLXtYjO738x+bWbPq1bh1bB06VKe/vSnc9xxx3HJJZfM\n6Xs9UmuEd7zjHdx4440cd9xxXH311SxbtozW1lZ+9atfcfLJJ3P88cfzsY99bI9X2Z1yyin893//\n937V8uMf/5jnPOc5nHDCCXznO9/hjW98IwC33347q1evfsSBXx7l+fLeWqD84lJ2sXU1Vq8l0EK3\n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\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig, ax = plt.subplots(figsize=(10, 5))\n", "s.plot(kind='bar', ax=ax, rot=45)\n", "plt.legend(loc='upper left')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Conclusions " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So here's the list of the things I learnt while doing this work:\n", "\n", "- declaring complex variables within Cython functions\n", "- using C++ std lib functions using cdef extern in Cython code\n", "- Cython allows one to easily parallelize code\n", "- the Cython error messages are often cryptic\n", "- it is useful to read the generated Cython code to understand what's going on under the hood of the translation" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.6.1" }, "toc": { "colors": { "hover_highlight": "#DAA520", "navigate_num": "#000000", "navigate_text": "#333333", "running_highlight": "#FF0000", "selected_highlight": "#FFD700", "sidebar_border": "#EEEEEE", "wrapper_background": "#FFFFFF" }, "moveMenuLeft": true, "nav_menu": { "height": "154px", "width": "253px" }, "navigate_menu": true, "number_sections": true, "sideBar": true, "threshold": 4, "toc_cell": false, "toc_section_display": "block", "toc_window_display": false, "widenNotebook": false } }, "nbformat": 4, "nbformat_minor": 1 }